opti : shaders rework test
This commit is contained in:
@@ -3,6 +3,46 @@
|
||||
|
||||
#include "Assets/_Content/Shaders/Include/Utilities.hlsl"
|
||||
|
||||
/*
|
||||
DETAILS - MEDIUM LOD VERSION
|
||||
|
||||
Goal:
|
||||
Keep the large performance gain from the optimized triplanar version,
|
||||
while restoring the old distance-based "detail upscale" behavior with smoother transitions.
|
||||
|
||||
Main changes compared to the original version:
|
||||
- Replaced the 7-direction projection system with standard 3-axis triplanar mapping.
|
||||
- Removed the expensive 2x2 texture bombing by default.
|
||||
- Restored manual distance-based detail scaling.
|
||||
- Reduced the LOD ratio from 4.0 to 2.0 to make transitions less visible.
|
||||
- Added stable spatial jitter to avoid the whole screen switching LOD at the same distance.
|
||||
- Kept the public function signature SampleProjectedDetailLOD(...)
|
||||
so existing Shader Graph nodes do not break.
|
||||
|
||||
Original cost per detail layer:
|
||||
7 projections * 2 LOD levels * 4 bombing samples = 56 texture samples.
|
||||
|
||||
This version:
|
||||
3 projections * 2 LOD levels = 6 texture samples.
|
||||
|
||||
If EnkiBillal samples both DetailTexA and DetailTexB:
|
||||
6 * 2 = 12 texture samples.
|
||||
Still much cheaper than the original 112 samples.
|
||||
*/
|
||||
|
||||
#define DETAILS_USE_LIGHT_BOMBING 0
|
||||
|
||||
/*
|
||||
Distance-based detail LOD.
|
||||
|
||||
This restores the old idea:
|
||||
- close camera -> smaller, sharper details;
|
||||
- far camera -> larger, more readable details.
|
||||
|
||||
Difference from the original:
|
||||
lodRatio is reduced from 4.0 to 2.0.
|
||||
This makes each transition less visible.
|
||||
*/
|
||||
void GetDetailLOD
|
||||
(
|
||||
float Depth,
|
||||
@@ -13,81 +53,140 @@ void GetDetailLOD
|
||||
out float LODBlend
|
||||
)
|
||||
{
|
||||
const float lodRatio = 4;
|
||||
const float lodSize = DetailSizeA / lodRatio * 2;
|
||||
const float lodRatio = 2.0;
|
||||
const float baseScale = 1.0;
|
||||
|
||||
float dist = max(Depth, 1e-5);
|
||||
float lod = log(dist) / log(lodRatio) + LODJitter; // log2(dist);
|
||||
|
||||
float lod = log(dist) / log(lodRatio) + LODJitter;
|
||||
|
||||
float lodFloor = floor(lod);
|
||||
float lodFrac = lod - lodFloor;
|
||||
|
||||
LODBlend = lodFrac * lodFrac * (3 - 2 * lodFrac);
|
||||
/*
|
||||
Quintic smootherstep.
|
||||
This is smoother than classic smoothstep and helps hide the LOD blend.
|
||||
*/
|
||||
float t = saturate(lodFrac);
|
||||
LODBlend = t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
|
||||
|
||||
DetailSizeAA = lodSize * pow(lodRatio, lodFloor);
|
||||
DetailSizeAB = lodSize * pow(lodRatio, lodFloor + 1);
|
||||
DetailSizeAA = DetailSizeA * baseScale * pow(lodRatio, lodFloor);
|
||||
DetailSizeAB = DetailSizeA * baseScale * pow(lodRatio, lodFloor + 1.0);
|
||||
}
|
||||
|
||||
/*
|
||||
Default fast texture sample.
|
||||
Uses normal GPU mipmapping.
|
||||
*/
|
||||
float4 SampleDetailTextureFast
|
||||
(
|
||||
UnityTexture2D Tex,
|
||||
float2 UV
|
||||
)
|
||||
{
|
||||
return Tex.tex.Sample(Tex.samplerstate, UV);
|
||||
}
|
||||
|
||||
/*
|
||||
Optional lightweight texture bombing.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
Compared to the original SampleBombedTexture:
|
||||
- original: 4 texture samples + 4 rotations + 4 weights;
|
||||
- this version: 1 texture sample with stable per-cell offset and rotation.
|
||||
|
||||
Warning:
|
||||
This can create visible cell transitions because neighboring cells are not blended.
|
||||
*/
|
||||
float4 SampleDetailTextureLightBombed
|
||||
(
|
||||
UnityTexture2D Tex,
|
||||
float2 UV
|
||||
)
|
||||
{
|
||||
float2 cell = floor(UV);
|
||||
|
||||
float2 randomOffset = (Hash2D(cell) - 0.5) * 0.35;
|
||||
float randomAngle = Hash1From2D(cell) * TWO_PI;
|
||||
|
||||
float2 sampleUV = UV + randomOffset;
|
||||
|
||||
sampleUV = RotateUV
|
||||
(
|
||||
sampleUV,
|
||||
cell + 0.5,
|
||||
randomAngle
|
||||
);
|
||||
|
||||
return Tex.tex.Sample(Tex.samplerstate, sampleUV);
|
||||
}
|
||||
|
||||
/*
|
||||
Compatibility function.
|
||||
|
||||
Existing code can still call SampleBombedTexture.
|
||||
In Medium mode, it routes to:
|
||||
- direct sampling by default;
|
||||
- optional one-sample light bombing if enabled.
|
||||
*/
|
||||
float4 SampleBombedTexture
|
||||
(
|
||||
UnityTexture2D Tex,
|
||||
float2 UV
|
||||
)
|
||||
{
|
||||
const float cellBlend = 4;
|
||||
|
||||
float2 cell = floor(UV);
|
||||
float2 local = frac(UV);
|
||||
|
||||
float4 result = 0;
|
||||
float weightSum = 0;
|
||||
|
||||
[unroll]
|
||||
for (int y = 0; y < 2; y++)
|
||||
{
|
||||
[unroll]
|
||||
for (int x = 0; x < 2; x++)
|
||||
{
|
||||
float2 currentCell = cell + float2(x, y);
|
||||
|
||||
float2 randomOffset = (Hash2D(currentCell) - .5) * .35;
|
||||
float randomAngle = Hash1From2D(currentCell) * TWO_PI;
|
||||
|
||||
float2 center = float2(x, y) + randomOffset;
|
||||
float2 delta = local - center;
|
||||
|
||||
float weight =
|
||||
pow(saturate(1 - abs(delta.x)), cellBlend) *
|
||||
pow(saturate(1 - abs(delta.y)), cellBlend);
|
||||
|
||||
float2 sampleUV = UV + randomOffset;
|
||||
|
||||
sampleUV = RotateUV
|
||||
(
|
||||
sampleUV,
|
||||
currentCell + .5,
|
||||
randomAngle
|
||||
);
|
||||
|
||||
result += Tex.tex.Sample
|
||||
(
|
||||
Tex.samplerstate,
|
||||
sampleUV
|
||||
) * weight;
|
||||
|
||||
weightSum += weight;
|
||||
}
|
||||
}
|
||||
|
||||
return result / max(weightSum, 1e-5);
|
||||
#if DETAILS_USE_LIGHT_BOMBING
|
||||
return SampleDetailTextureLightBombed(Tex, UV);
|
||||
#else
|
||||
return SampleDetailTextureFast(Tex, UV);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
Triplanar weights.
|
||||
|
||||
ProjectionBlend keeps the same artistic role:
|
||||
- low value -> softer blend between axes;
|
||||
- high value -> sharper projection selection.
|
||||
|
||||
The original shader used up to 32.
|
||||
This version uses 16 by default to reduce overly sharp transitions.
|
||||
*/
|
||||
float3 GetTriplanarWeights
|
||||
(
|
||||
float3 NormalOS,
|
||||
float ProjectionBlend
|
||||
)
|
||||
{
|
||||
float3 n = abs(normalize(NormalOS));
|
||||
|
||||
float sharpness = lerp(2.0, 16.0, saturate(ProjectionBlend));
|
||||
|
||||
float3 w = pow(saturate(n), sharpness);
|
||||
float sumW = max(w.x + w.y + w.z, 1e-5);
|
||||
|
||||
return w / sumW;
|
||||
}
|
||||
|
||||
/*
|
||||
Compatibility structure.
|
||||
|
||||
Kept because old code or Shader Graph nodes may still expect this type.
|
||||
The optimized SampleProjectedDetailLOD below does not use the old 7-projection path.
|
||||
*/
|
||||
struct ProjectionData
|
||||
{
|
||||
float2 uv[7];
|
||||
float mask[7];
|
||||
};
|
||||
|
||||
/*
|
||||
Compatibility fallback.
|
||||
|
||||
Fills only the first three entries using standard triplanar projections.
|
||||
Entries 3..6 are disabled.
|
||||
*/
|
||||
ProjectionData GetProjections
|
||||
(
|
||||
float3 PositionOS,
|
||||
@@ -97,57 +196,72 @@ ProjectionData GetProjections
|
||||
{
|
||||
ProjectionData data;
|
||||
|
||||
float3 normal = normalize(NormalOS);
|
||||
float3 w = GetTriplanarWeights(NormalOS, ProjectionBlend);
|
||||
|
||||
float3 projectionNormals[7] =
|
||||
{
|
||||
normalize(float3(1, 1, 1)),
|
||||
normalize(float3(-1, 1, 1)),
|
||||
normalize(float3(1, -1, 1)),
|
||||
normalize(float3(-1, -1, 1)),
|
||||
float3(1, 0, 0),
|
||||
float3(0, 1, 0),
|
||||
float3(0, 0, 1)
|
||||
};
|
||||
/*
|
||||
Standard triplanar UVs:
|
||||
- X projection samples the YZ plane.
|
||||
- Y projection samples the XZ plane.
|
||||
- Z projection samples the XY plane.
|
||||
*/
|
||||
data.uv[0] = PositionOS.zy;
|
||||
data.uv[1] = PositionOS.xz;
|
||||
data.uv[2] = PositionOS.xy;
|
||||
|
||||
float sharpness = lerp(2, 32, saturate(ProjectionBlend));
|
||||
|
||||
float maskSum = 0;
|
||||
data.mask[0] = w.x;
|
||||
data.mask[1] = w.y;
|
||||
data.mask[2] = w.z;
|
||||
|
||||
[unroll]
|
||||
for (int i = 0; i < 7; i++)
|
||||
for (int i = 3; i < 7; i++)
|
||||
{
|
||||
float3 n = projectionNormals[i];
|
||||
|
||||
float d = abs(dot(normal, n));
|
||||
|
||||
data.mask[i] = pow(saturate(d), sharpness);
|
||||
maskSum += data.mask[i];
|
||||
|
||||
float3 up = abs(n.y) < .999
|
||||
? float3(0, 1, 0)
|
||||
: float3(1, 0, 0);
|
||||
|
||||
float3 tangent = normalize(cross(up, n));
|
||||
float3 bitangent = normalize(cross(n, tangent));
|
||||
|
||||
data.uv[i] = float2
|
||||
(
|
||||
dot(PositionOS, tangent),
|
||||
dot(PositionOS, bitangent)
|
||||
);
|
||||
data.uv[i] = 0.0;
|
||||
data.mask[i] = 0.0;
|
||||
}
|
||||
|
||||
maskSum = max(maskSum, 1e-5);
|
||||
|
||||
[unroll]
|
||||
for (int i = 0; i < 7; i++)
|
||||
data.mask[i] /= maskSum;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
Samples one triplanar axis with two distance-based detail sizes.
|
||||
|
||||
This restores the "detail upscale with distance" effect without returning
|
||||
to the original 7-projection and 2x2 bombing cost.
|
||||
*/
|
||||
float4 SampleDetailAxisLOD
|
||||
(
|
||||
UnityTexture2D DetailTex,
|
||||
float2 BaseUV,
|
||||
float DetailSizeAA,
|
||||
float DetailSizeAB,
|
||||
float LODBlend
|
||||
)
|
||||
{
|
||||
float4 detailA = SampleBombedTexture
|
||||
(
|
||||
DetailTex,
|
||||
BaseUV / max(DetailSizeAA, 1e-5)
|
||||
);
|
||||
|
||||
float4 detailB = SampleBombedTexture
|
||||
(
|
||||
DetailTex,
|
||||
BaseUV / max(DetailSizeAB, 1e-5)
|
||||
);
|
||||
|
||||
return lerp(detailA, detailB, LODBlend);
|
||||
}
|
||||
|
||||
/*
|
||||
Optimized projected detail sampling with smoother distance LOD.
|
||||
|
||||
Cost:
|
||||
3 triplanar axes * 2 LOD samples = 6 texture samples.
|
||||
|
||||
The old version also added per-projection jitter.
|
||||
This version uses one stable object-space jitter for the whole sample.
|
||||
That keeps the transition less synchronized without reintroducing too much cost.
|
||||
*/
|
||||
float4 SampleProjectedDetailLOD
|
||||
(
|
||||
UnityTexture2D DetailTex,
|
||||
@@ -158,57 +272,72 @@ float4 SampleProjectedDetailLOD
|
||||
float Depth
|
||||
)
|
||||
{
|
||||
const float lodJitterStrength = .8;
|
||||
const float lodJitterSize = 1.5;
|
||||
float detailSize = max(DetailSizeA, 1e-5);
|
||||
|
||||
float4 detail = 0;
|
||||
float totalMask = 0;
|
||||
float3 weights = GetTriplanarWeights(NormalOS, ProjectionBlend);
|
||||
|
||||
ProjectionData projections = GetProjections
|
||||
/*
|
||||
Stable spatial jitter.
|
||||
|
||||
This prevents the whole screen from switching detail scale at exactly
|
||||
the same camera distance.
|
||||
|
||||
Keep this value low.
|
||||
Too much jitter makes the transition patchy.
|
||||
*/
|
||||
float lodJitter = (Hash1From2D(floor(PositionOS.xz * 0.75)) - 0.5) * 0.35;
|
||||
|
||||
float DetailSizeAA;
|
||||
float DetailSizeAB;
|
||||
float lodBlend;
|
||||
|
||||
GetDetailLOD
|
||||
(
|
||||
PositionOS,
|
||||
NormalOS,
|
||||
ProjectionBlend
|
||||
Depth,
|
||||
detailSize,
|
||||
lodJitter,
|
||||
DetailSizeAA,
|
||||
DetailSizeAB,
|
||||
lodBlend
|
||||
);
|
||||
|
||||
[unroll]
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
float mask = projections.mask[i];
|
||||
/*
|
||||
Triplanar base UVs in object space.
|
||||
*/
|
||||
float2 uvX = PositionOS.zy;
|
||||
float2 uvY = PositionOS.xz;
|
||||
float2 uvZ = PositionOS.xy;
|
||||
|
||||
float lodJitter = (Hash1From2D(floor(projections.uv[i] * lodJitterSize) + i * 17.13) - .5) * lodJitterStrength;
|
||||
float4 sampleX = SampleDetailAxisLOD
|
||||
(
|
||||
DetailTex,
|
||||
uvX,
|
||||
DetailSizeAA,
|
||||
DetailSizeAB,
|
||||
lodBlend
|
||||
);
|
||||
|
||||
float DetailSizeAA;
|
||||
float DetailSizeAB;
|
||||
float lodBlend;
|
||||
float4 sampleY = SampleDetailAxisLOD
|
||||
(
|
||||
DetailTex,
|
||||
uvY,
|
||||
DetailSizeAA,
|
||||
DetailSizeAB,
|
||||
lodBlend
|
||||
);
|
||||
|
||||
GetDetailLOD
|
||||
(
|
||||
Depth,
|
||||
DetailSizeA,
|
||||
lodJitter,
|
||||
DetailSizeAA,
|
||||
DetailSizeAB,
|
||||
lodBlend
|
||||
);
|
||||
float4 sampleZ = SampleDetailAxisLOD
|
||||
(
|
||||
DetailTex,
|
||||
uvZ,
|
||||
DetailSizeAA,
|
||||
DetailSizeAB,
|
||||
lodBlend
|
||||
);
|
||||
|
||||
float4 detailA = SampleBombedTexture
|
||||
(
|
||||
DetailTex,
|
||||
projections.uv[i] / DetailSizeAA
|
||||
);
|
||||
|
||||
float4 detailB = SampleBombedTexture
|
||||
(
|
||||
DetailTex,
|
||||
projections.uv[i] / DetailSizeAB
|
||||
);
|
||||
|
||||
detail += lerp(detailA, detailB, lodBlend) * mask;
|
||||
totalMask += mask;
|
||||
}
|
||||
|
||||
return detail / max(totalMask, 1e-5);
|
||||
return sampleX * weights.x
|
||||
+ sampleY * weights.y
|
||||
+ sampleZ * weights.z;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3d6911aa39004b4a88bf6731817967c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,214 @@
|
||||
#ifndef DETAILS_HLSL
|
||||
#define DETAILS_HLSL
|
||||
|
||||
#include "Assets/_Content/Shaders/Include/Utilities.hlsl"
|
||||
|
||||
void GetDetailLOD
|
||||
(
|
||||
float Depth,
|
||||
float DetailSizeA,
|
||||
float LODJitter,
|
||||
out float DetailSizeAA,
|
||||
out float DetailSizeAB,
|
||||
out float LODBlend
|
||||
)
|
||||
{
|
||||
const float lodRatio = 4;
|
||||
const float lodSize = DetailSizeA / lodRatio * 2;
|
||||
|
||||
float dist = max(Depth, 1e-5);
|
||||
float lod = log(dist) / log(lodRatio) + LODJitter; // log2(dist);
|
||||
|
||||
float lodFloor = floor(lod);
|
||||
float lodFrac = lod - lodFloor;
|
||||
|
||||
LODBlend = lodFrac * lodFrac * (3 - 2 * lodFrac);
|
||||
|
||||
DetailSizeAA = lodSize * pow(lodRatio, lodFloor);
|
||||
DetailSizeAB = lodSize * pow(lodRatio, lodFloor + 1);
|
||||
}
|
||||
|
||||
float4 SampleBombedTexture
|
||||
(
|
||||
UnityTexture2D Tex,
|
||||
float2 UV
|
||||
)
|
||||
{
|
||||
const float cellBlend = 4;
|
||||
|
||||
float2 cell = floor(UV);
|
||||
float2 local = frac(UV);
|
||||
|
||||
float4 result = 0;
|
||||
float weightSum = 0;
|
||||
|
||||
[unroll]
|
||||
for (int y = 0; y < 2; y++)
|
||||
{
|
||||
[unroll]
|
||||
for (int x = 0; x < 2; x++)
|
||||
{
|
||||
float2 currentCell = cell + float2(x, y);
|
||||
|
||||
float2 randomOffset = (Hash2D(currentCell) - .5) * .35;
|
||||
float randomAngle = Hash1From2D(currentCell) * TWO_PI;
|
||||
|
||||
float2 center = float2(x, y) + randomOffset;
|
||||
float2 delta = local - center;
|
||||
|
||||
float weight =
|
||||
pow(saturate(1 - abs(delta.x)), cellBlend) *
|
||||
pow(saturate(1 - abs(delta.y)), cellBlend);
|
||||
|
||||
float2 sampleUV = UV + randomOffset;
|
||||
|
||||
sampleUV = RotateUV
|
||||
(
|
||||
sampleUV,
|
||||
currentCell + .5,
|
||||
randomAngle
|
||||
);
|
||||
|
||||
result += Tex.tex.Sample
|
||||
(
|
||||
Tex.samplerstate,
|
||||
sampleUV
|
||||
) * weight;
|
||||
|
||||
weightSum += weight;
|
||||
}
|
||||
}
|
||||
|
||||
return result / max(weightSum, 1e-5);
|
||||
}
|
||||
|
||||
struct ProjectionData
|
||||
{
|
||||
float2 uv[7];
|
||||
float mask[7];
|
||||
};
|
||||
|
||||
ProjectionData GetProjections
|
||||
(
|
||||
float3 PositionOS,
|
||||
float3 NormalOS,
|
||||
float ProjectionBlend
|
||||
)
|
||||
{
|
||||
ProjectionData data;
|
||||
|
||||
float3 normal = normalize(NormalOS);
|
||||
|
||||
float3 projectionNormals[7] =
|
||||
{
|
||||
normalize(float3(1, 1, 1)),
|
||||
normalize(float3(-1, 1, 1)),
|
||||
normalize(float3(1, -1, 1)),
|
||||
normalize(float3(-1, -1, 1)),
|
||||
float3(1, 0, 0),
|
||||
float3(0, 1, 0),
|
||||
float3(0, 0, 1)
|
||||
};
|
||||
|
||||
float sharpness = lerp(2, 32, saturate(ProjectionBlend));
|
||||
|
||||
float maskSum = 0;
|
||||
|
||||
[unroll]
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
float3 n = projectionNormals[i];
|
||||
|
||||
float d = abs(dot(normal, n));
|
||||
|
||||
data.mask[i] = pow(saturate(d), sharpness);
|
||||
maskSum += data.mask[i];
|
||||
|
||||
float3 up = abs(n.y) < .999
|
||||
? float3(0, 1, 0)
|
||||
: float3(1, 0, 0);
|
||||
|
||||
float3 tangent = normalize(cross(up, n));
|
||||
float3 bitangent = normalize(cross(n, tangent));
|
||||
|
||||
data.uv[i] = float2
|
||||
(
|
||||
dot(PositionOS, tangent),
|
||||
dot(PositionOS, bitangent)
|
||||
);
|
||||
}
|
||||
|
||||
maskSum = max(maskSum, 1e-5);
|
||||
|
||||
[unroll]
|
||||
for (int i = 0; i < 7; i++)
|
||||
data.mask[i] /= maskSum;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
float4 SampleProjectedDetailLOD
|
||||
(
|
||||
UnityTexture2D DetailTex,
|
||||
float3 PositionOS,
|
||||
float3 NormalOS,
|
||||
float ProjectionBlend,
|
||||
float DetailSizeA,
|
||||
float Depth
|
||||
)
|
||||
{
|
||||
const float lodJitterStrength = .8;
|
||||
const float lodJitterSize = 1.5;
|
||||
|
||||
float4 detail = 0;
|
||||
float totalMask = 0;
|
||||
|
||||
ProjectionData projections = GetProjections
|
||||
(
|
||||
PositionOS,
|
||||
NormalOS,
|
||||
ProjectionBlend
|
||||
);
|
||||
|
||||
[unroll]
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
float mask = projections.mask[i];
|
||||
|
||||
float lodJitter = (Hash1From2D(floor(projections.uv[i] * lodJitterSize) + i * 17.13) - .5) * lodJitterStrength;
|
||||
|
||||
float DetailSizeAA;
|
||||
float DetailSizeAB;
|
||||
float lodBlend;
|
||||
|
||||
GetDetailLOD
|
||||
(
|
||||
Depth,
|
||||
DetailSizeA,
|
||||
lodJitter,
|
||||
DetailSizeAA,
|
||||
DetailSizeAB,
|
||||
lodBlend
|
||||
);
|
||||
|
||||
float4 detailA = SampleBombedTexture
|
||||
(
|
||||
DetailTex,
|
||||
projections.uv[i] / DetailSizeAA
|
||||
);
|
||||
|
||||
float4 detailB = SampleBombedTexture
|
||||
(
|
||||
DetailTex,
|
||||
projections.uv[i] / DetailSizeAB
|
||||
);
|
||||
|
||||
detail += lerp(detailA, detailB, lodBlend) * mask;
|
||||
totalMask += mask;
|
||||
}
|
||||
|
||||
return detail / max(totalMask, 1e-5);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4dfd3fe541362f4f8143c626427c71f
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user