mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-12 08:11:10 +02:00
Secondary bounce lighting now uses a stable per-hit RNG for sky/direct-light correction instead of frame-varying RNG, so bounce lighting should stop crawling/sparkling as much.
This commit is contained in:
@@ -561,6 +561,8 @@ void idRenderModelStatic::UpdateDXR(uint32_t& dxrBottomAcel, int onlySurface)
|
||||
{
|
||||
int numDXRVerts = 0;
|
||||
int numDXRIndexes = 0;
|
||||
float averageColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
float averageColorWeight = 0.0f;
|
||||
|
||||
for (int i = 0; i < surfaces.Num(); i++)
|
||||
{
|
||||
@@ -580,6 +582,21 @@ void idRenderModelStatic::UpdateDXR(uint32_t& dxrBottomAcel, int onlySurface)
|
||||
|
||||
numDXRVerts += surf->geometry->numVerts;
|
||||
numDXRIndexes += surf->geometry->numIndexes;
|
||||
|
||||
if (surf->shader)
|
||||
{
|
||||
idImage* diffuseImage = surf->shader->GetDiffuseImage(NULL);
|
||||
if (diffuseImage)
|
||||
{
|
||||
const float* c = diffuseImage->GetAverageColor();
|
||||
const float weight = (surf->geometry->numIndexes > 0) ? (float)surf->geometry->numIndexes : 1.0f;
|
||||
averageColor[0] += c[0] * weight;
|
||||
averageColor[1] += c[1] * weight;
|
||||
averageColor[2] += c[2] * weight;
|
||||
averageColor[3] += c[3] * weight;
|
||||
averageColorWeight += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<glRaytracingVertex_t> vertices(numDXRVerts);
|
||||
@@ -644,6 +661,14 @@ void idRenderModelStatic::UpdateDXR(uint32_t& dxrBottomAcel, int onlySurface)
|
||||
desc.averageColor[1] = 1.0f;
|
||||
desc.averageColor[2] = 1.0f;
|
||||
desc.averageColor[3] = 1.0f;
|
||||
if (averageColorWeight > 0.0f)
|
||||
{
|
||||
const float invWeight = 1.0f / averageColorWeight;
|
||||
desc.averageColor[0] = idMath::ClampFloat(0.0f, 1.0f, averageColor[0] * invWeight);
|
||||
desc.averageColor[1] = idMath::ClampFloat(0.0f, 1.0f, averageColor[1] * invWeight);
|
||||
desc.averageColor[2] = idMath::ClampFloat(0.0f, 1.0f, averageColor[2] * invWeight);
|
||||
desc.averageColor[3] = idMath::ClampFloat(0.0f, 1.0f, averageColor[3] * invWeight);
|
||||
}
|
||||
|
||||
if (!dxrBottomAcel)
|
||||
{
|
||||
|
||||
@@ -2863,6 +2863,52 @@ float3 EnhanceBumpNormal(uint2 pixel, float3 worldPos, float3 baseAlbedo, bool i
|
||||
return outN;
|
||||
}
|
||||
|
||||
float3 StabilizeSkeletalNormal(uint2 pixel, float3 worldPos, float3 N)
|
||||
{
|
||||
int2 p = int2(pixel);
|
||||
int2 maxP = int2((int)gScreenSize.x - 1, (int)gScreenSize.y - 1);
|
||||
|
||||
float3 accum = N * 3.0;
|
||||
float weightSum = 3.0;
|
||||
|
||||
[unroll]
|
||||
for (int y = -1; y <= 1; ++y)
|
||||
{
|
||||
[unroll]
|
||||
for (int x = -1; x <= 1; ++x)
|
||||
{
|
||||
if (x == 0 && y == 0)
|
||||
continue;
|
||||
|
||||
int2 sp = clamp(p + int2(x, y), int2(0, 0), maxP);
|
||||
float4 samplePos = gPositionTex.Load(int3(sp, 0));
|
||||
uint sampleGeoFlag = DecodeGeometryFlag(samplePos.w);
|
||||
if ((sampleGeoFlag & GEOMETRY_FLAG_SKELETAL) == 0u)
|
||||
continue;
|
||||
|
||||
float3 sampleDelta = samplePos.xyz - worldPos;
|
||||
float distSq = dot(sampleDelta, sampleDelta);
|
||||
if (distSq > 16.0)
|
||||
continue;
|
||||
|
||||
float3 sampleN = SafeNormalizeLocal(gNormalTex.Load(int3(sp, 0)).xyz, N);
|
||||
if (dot(sampleN, N) < 0.0)
|
||||
sampleN = -sampleN;
|
||||
|
||||
float nd = saturate(dot(sampleN, N));
|
||||
if (nd < 0.20)
|
||||
continue;
|
||||
|
||||
float w = nd * rcp(1.0 + distSq * 0.35);
|
||||
accum += sampleN * w;
|
||||
weightSum += w;
|
||||
}
|
||||
}
|
||||
|
||||
float3 smoothed = SafeNormalizeLocal(accum / max(weightSum, 1.0e-4), N);
|
||||
return SafeNormalizeLocal(lerp(N, smoothed, 0.70), N);
|
||||
}
|
||||
|
||||
[shader("miss")]
|
||||
void ShadowMiss(inout ShadowPayload payload)
|
||||
{
|
||||
@@ -3112,7 +3158,8 @@ float GetPointLightMaxRadius(Light Lgt)
|
||||
float3 r = GetPointLightRadius(Lgt);
|
||||
return max(max(r.x, r.y), r.z);
|
||||
}
|
||||
|
||||
)"
|
||||
R"(
|
||||
float3 Doom3SafeNormalizeOr(float3 v, float3 fallbackDir)
|
||||
{
|
||||
float lenSq = dot(v, v);
|
||||
@@ -3819,6 +3866,17 @@ uint InitRng(uint2 pixel, uint frameIndex, uint sampleIndex)
|
||||
return PcgHash(seed) | 1u;
|
||||
}
|
||||
|
||||
uint InitSpatialRng(uint2 pixel, float3 worldPos, uint salt)
|
||||
{
|
||||
uint seed = pixel.x * 1973u;
|
||||
seed ^= pixel.y * 9277u;
|
||||
seed ^= asuint(worldPos.x * 0.03125) * 1597334677u;
|
||||
seed ^= asuint(worldPos.y * 0.03125) * 3812015801u;
|
||||
seed ^= asuint(worldPos.z * 0.03125) * 2798796415u;
|
||||
seed ^= salt * 374761393u;
|
||||
return PcgHash(seed) | 1u;
|
||||
}
|
||||
|
||||
float Rand(inout uint rng)
|
||||
{
|
||||
rng = PcgHash(rng);
|
||||
@@ -4542,6 +4600,24 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
float3 EstimateUnresolvedBounceRadiance(float3 hitPos, float3 hitN, float3 albedo)
|
||||
{
|
||||
hitN = SafeNormalizeOr(hitN, float3(0.0, 0.0, 1.0));
|
||||
albedo = max(saturate(albedo), float3(0.03, 0.03, 0.03));
|
||||
|
||||
float upness = saturate(hitN.z * 0.5 + 0.5);
|
||||
float3 lighting = gAmbientColor.rgb * (gAmbientColor.a * 0.025);
|
||||
lighting += GetSkyRadiance(hitN) * (0.035 + 0.035 * upness);
|
||||
|
||||
[loop]
|
||||
for (uint i = 0; i < gLightCount; ++i)
|
||||
{
|
||||
lighting += EstimateFastBounceLight(hitPos, hitN, gLights[i]) * 0.35;
|
||||
}
|
||||
|
||||
return clamp(albedo * max(lighting, 0.0), 0.0, 6.0);
|
||||
}
|
||||
|
||||
float3 EstimateShadowedBounceLight(uint2 hitPixel, float3 hitPos, float3 hitN, float3 hitV, float3 hitAlbedo, Light Lgt, inout uint rng)
|
||||
{
|
||||
float3 spec = 0.0;
|
||||
@@ -4596,8 +4672,11 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
|
||||
{
|
||||
bool hitIsSkeletal = (hitGeoFlag & GEOMETRY_FLAG_SKELETAL) != 0u;
|
||||
bool hitIsUnlit = (hitGeoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
|
||||
uint lightingRng = InitSpatialRng(hitPixel, hitPos, 0xB04157u);
|
||||
|
||||
hitN = SafeNormalizeOr(hitN, float3(0.0, 0.0, 1.0));
|
||||
if (hitIsSkeletal)
|
||||
hitN = StabilizeSkeletalNormal(hitPixel, hitPos, hitN);
|
||||
hitV = SafeNormalizeOr(hitV, -hitN);
|
||||
hitAlbedo = saturate(hitAlbedo);
|
||||
|
||||
@@ -4611,7 +4690,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
|
||||
|
||||
// Real occluded sky contribution at the secondary hit. The previous GI path
|
||||
// used a fixed sky term, so corners/cavities received too much indirect light.
|
||||
lighting += EstimateBounceSkyLighting(hitPos, hitN, rng) * (0.14 + 0.10 * upness);
|
||||
lighting += EstimateBounceSkyLighting(hitPos, hitN, lightingRng) * (0.14 + 0.10 * upness);
|
||||
|
||||
// Glow-map emissive no longer participates in secondary GI. It remains a
|
||||
// direct visible/bloom-only effect until real material-space emissive lighting
|
||||
@@ -4628,10 +4707,9 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
|
||||
}
|
||||
lighting += fastAllLights;
|
||||
|
||||
// Visibility correction: replace a small rotating subset of the unshadowed
|
||||
// baseline with real shadowed direct lighting. Temporal accumulation in the
|
||||
// compute pass below makes this converge without tracing every light at every
|
||||
// bounce hit.
|
||||
// Visibility correction: replace a small stable subset of the unshadowed
|
||||
// baseline with real shadowed direct lighting. Keying this to the hit
|
||||
// position avoids frame-to-frame sparkle while keeping the all-lights bounce.
|
||||
uint correctionBudget = 0u;
|
||||
if (gLightCount > 0u)
|
||||
{
|
||||
@@ -4642,10 +4720,10 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
|
||||
|
||||
if (correctionBudget > 0u)
|
||||
{
|
||||
uint start = PcgHash(rng ^ 0x9E3779B9u) % gLightCount;
|
||||
rng = PcgHash(rng + 0xBB67AE85u);
|
||||
uint stride = (gLightCount > 1u) ? (1u + (PcgHash(rng ^ 0x3C6EF372u) % (gLightCount - 1u))) : 1u;
|
||||
rng = PcgHash(rng + 0xA54FF53Au);
|
||||
uint start = PcgHash(lightingRng ^ 0x9E3779B9u) % gLightCount;
|
||||
lightingRng = PcgHash(lightingRng + 0xBB67AE85u);
|
||||
uint stride = (gLightCount > 1u) ? (1u + (PcgHash(lightingRng ^ 0x3C6EF372u) % (gLightCount - 1u))) : 1u;
|
||||
lightingRng = PcgHash(lightingRng + 0xA54FF53Au);
|
||||
|
||||
[loop]
|
||||
for (uint c = 0u; c < correctionBudget; ++c)
|
||||
@@ -4653,7 +4731,7 @@ float3 EstimateDirectLightingForBounceHit(uint2 hitPixel, float3 hitPos, float3
|
||||
uint lightIndex = (start + c * stride) % gLightCount;
|
||||
Light Lgt = gLights[lightIndex];
|
||||
float3 fast = EstimateFastBounceLight(hitPos, hitN, Lgt);
|
||||
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, Lgt, rng);
|
||||
float3 shadowed = EstimateShadowedBounceLight(hitPixel, hitPos, hitN, hitV, hitAlbedo, Lgt, lightingRng);
|
||||
lighting += shadowed - fast;
|
||||
}
|
||||
}
|
||||
@@ -4866,9 +4944,9 @@ float3 TraceOneIndirectBouncePath(uint2 pixel, float3 worldPos, float3 N, float3
|
||||
uint2 hitPixel = pathPixel;
|
||||
float3 hitPos = rayHitPos;
|
||||
float3 hitNormal = SafeNormalizeOr(-bounceDir, pathNormal);
|
||||
float3 hitAlbedo = float3(0.55, 0.55, 0.55);
|
||||
uint hitGeoFlag = GEOMETRY_FLAG_NONE;
|
||||
float3 meshAverageColor = GetMeshAverageColor(meshIndex);
|
||||
float3 hitAlbedo = meshAverageColor;
|
||||
uint hitGeoFlag = GEOMETRY_FLAG_NONE;
|
||||
|
||||
bool hasGBufferMaterial = TryFetchGBufferAtRayHit(
|
||||
rayHitPos,
|
||||
@@ -4885,25 +4963,30 @@ float3 TraceOneIndirectBouncePath(uint2 pixel, float3 worldPos, float3 N, float3
|
||||
hitNormal = -hitNormal;
|
||||
|
||||
float3 hitV = SafeNormalizeOr(-bounceDir, pathView);
|
||||
float3 bouncedRadiance = EstimateDirectLightingForBounceHit(
|
||||
hitPixel,
|
||||
hitPos,
|
||||
hitNormal,
|
||||
hitV,
|
||||
hitAlbedo,
|
||||
hitGeoFlag,
|
||||
rng);
|
||||
|
||||
if (!hasGBufferMaterial)
|
||||
float3 bouncedRadiance = 0.0;
|
||||
if (hasGBufferMaterial)
|
||||
{
|
||||
// The ray hit real TLAS geometry, but material lookup via camera
|
||||
// G-buffer failed because the surface is hidden/off-screen. Keep the
|
||||
// bounce alive with a neutral material and a little environment tint.
|
||||
float3 skyTint = GetSkyRadiance(SafeNormalizeOr(reflect(bounceDir, hitNormal), float3(0.0, 0.0, 1.0)));
|
||||
bouncedRadiance += skyTint * 0.045;
|
||||
bouncedRadiance = EstimateDirectLightingForBounceHit(
|
||||
hitPixel,
|
||||
hitPos,
|
||||
hitNormal,
|
||||
hitV,
|
||||
hitAlbedo,
|
||||
hitGeoFlag,
|
||||
rng);
|
||||
|
||||
bouncedRadiance *= meshAverageColor * 1.12;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The ray hit real TLAS geometry, but material lookup through the
|
||||
// camera G-buffer failed or landed on an unrelated visible pixel.
|
||||
// Use mesh-derived color instead of neutral grey/screen material.
|
||||
hitPos = rayHitPos;
|
||||
hitAlbedo = meshAverageColor;
|
||||
bouncedRadiance = EstimateUnresolvedBounceRadiance(hitPos, hitNormal, hitAlbedo);
|
||||
}
|
||||
|
||||
bouncedRadiance *= meshAverageColor * 1.12;
|
||||
bouncedRadiance = CompressEmissiveRadiance(bouncedRadiance, depth == 0u ? 8.0 : 5.0);
|
||||
accum += throughput * bouncedRadiance;
|
||||
|
||||
@@ -5360,8 +5443,9 @@ void RayGen()
|
||||
uint geoFlag = DecodeGeometryFlag(positionSample.w);
|
||||
bool isSkeletal = (geoFlag & GEOMETRY_FLAG_SKELETAL) != 0u;
|
||||
bool isUnlit = (geoFlag & GEOMETRY_FLAG_UNLIT) != 0u;
|
||||
float4 normalSample = LoadSceneNormal(pixel);
|
||||
float3 N = EnhanceBumpNormal(pixel, worldPos, baseAlbedo, isSkeletal);
|
||||
if (isSkeletal)
|
||||
N = StabilizeSkeletalNormal(pixel, worldPos, N);
|
||||
float3 V = normalize(gCameraPos.xyz - worldPos);
|
||||
|
||||
if (isUnlit)
|
||||
@@ -5538,6 +5622,8 @@ float Luminance(float3 c)
|
||||
return dot(c, float3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
static const uint GEOMETRY_FLAG_SKELETAL = 1u;
|
||||
static const uint GEOMETRY_FLAG_UNLIT = 2u;
|
||||
static const uint GEOMETRY_FLAG_GLASS = 4u;
|
||||
|
||||
uint DecodeGeometryFlag(float geoFlag)
|
||||
@@ -5599,9 +5685,10 @@ float GeometryAwareWeight(
|
||||
return 0.0;
|
||||
|
||||
// Do not smear lighting across material-class boundaries. This is
|
||||
// particularly important for glass, because the primary G-buffer sample can
|
||||
// be glass while the ray visibility must continue through it.
|
||||
if (((centerGeoFlag ^ sampleGeoFlag) & GEOMETRY_FLAG_GLASS) != 0u)
|
||||
// particularly important for skeletal pixels and glass, because both can sit
|
||||
// in front of unrelated world lighting in the same screen neighborhood.
|
||||
const uint classMask = GEOMETRY_FLAG_SKELETAL | GEOMETRY_FLAG_UNLIT | GEOMETRY_FLAG_GLASS;
|
||||
if (((centerGeoFlag ^ sampleGeoFlag) & classMask) != 0u)
|
||||
return 0.0;
|
||||
|
||||
float3 centerLighting = DemodulateLighting(centerRadiance, centerAlbedo);
|
||||
@@ -5736,6 +5823,16 @@ void DenoiseCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
int2 sp = int2(pixel) + int2(x, y);
|
||||
if (sp.x < 0 || sp.y < 0 || sp.x >= (int)gScreenSize.x || sp.y >= (int)gScreenSize.y)
|
||||
continue;
|
||||
float sampleDepth = gDepthTex.Load(int3(sp, 0));
|
||||
if (sampleDepth <= 0.0 || sampleDepth >= 1.0)
|
||||
continue;
|
||||
|
||||
float4 samplePos4 = gPositionTex.Load(int3(sp, 0));
|
||||
uint sampleGeoFlag = DecodeGeometryFlag(samplePos4.w);
|
||||
const uint classMask = GEOMETRY_FLAG_SKELETAL | GEOMETRY_FLAG_UNLIT | GEOMETRY_FLAG_GLASS;
|
||||
if (((centerGeoFlag ^ sampleGeoFlag) & classMask) != 0u)
|
||||
continue;
|
||||
|
||||
float3 raw = gPathTraceTex.Load(int3(sp, 0)).rgb;
|
||||
minRaw = min(minRaw, raw);
|
||||
maxRaw = max(maxRaw, raw);
|
||||
@@ -5801,6 +5898,11 @@ float3 SafeNormalTemporal(float3 n)
|
||||
return n * rsqrt(lenSq);
|
||||
}
|
||||
|
||||
uint DecodeGeometryFlagTemporal(float geoFlag)
|
||||
{
|
||||
return (uint)floor(max(geoFlag, 0.0) + 0.5);
|
||||
}
|
||||
|
||||
float3 ClampHistoryToCurrent(float3 history, float3 current)
|
||||
{
|
||||
// Clamp enough to kill rare GI/volume fireflies before they enter history,
|
||||
@@ -5819,6 +5921,9 @@ void TemporalAccumCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
|
||||
float4 raw = gPathTraceTex.Load(int3(pixel, 0));
|
||||
float depth = gDepthTex.Load(int3(pixel, 0));
|
||||
float4 positionSample = gPositionTex.Load(int3(pixel, 0));
|
||||
uint geoFlag = DecodeGeometryFlagTemporal(positionSample.w);
|
||||
bool isSkeletal = (geoFlag & 1u) != 0u;
|
||||
|
||||
if (depth <= 0.0 || depth >= 1.0 || gMaxBounces <= 1u)
|
||||
{
|
||||
@@ -5847,6 +5952,8 @@ void TemporalAccumCS(uint3 dispatchThreadId : SV_DispatchThreadID)
|
||||
// current-frame weight so the accumulator does not leave obvious trails.
|
||||
float currentWeight = max(1.0 / (historyCount + 1.0), 0.035);
|
||||
currentWeight = max(currentWeight, saturate(relChange * 0.24));
|
||||
if (isSkeletal)
|
||||
currentWeight = max(currentWeight, 0.25);
|
||||
currentWeight = saturate(currentWeight);
|
||||
|
||||
float3 resolved = lerp(historyColor, max(raw.rgb, 0.0), currentWeight);
|
||||
|
||||
@@ -361,6 +361,8 @@ void QD3D12_SetCameraInfo(
|
||||
const float* clipToView,
|
||||
const float* clipToPrevClip,
|
||||
const float* prevClipToClip,
|
||||
const float* worldToView,
|
||||
const float* viewToWorld,
|
||||
const float* cameraPos,
|
||||
const float* cameraRight,
|
||||
const float* cameraUp,
|
||||
@@ -534,6 +536,8 @@ struct QD3D12CameraState
|
||||
Mat4 clipToView = Mat4::Identity();
|
||||
Mat4 clipToPrevClip = Mat4::Identity();
|
||||
Mat4 prevClipToClip = Mat4::Identity();
|
||||
Mat4 worldToView = Mat4::Identity();
|
||||
Mat4 viewToWorld = Mat4::Identity();
|
||||
float cameraPos[3] = { 0.0f, 0.0f, 0.0f };
|
||||
float cameraRight[3] = { 1.0f, 0.0f, 0.0f };
|
||||
float cameraUp[3] = { 0.0f, 1.0f, 0.0f };
|
||||
@@ -2173,6 +2177,8 @@ static bool QD3D12_UpdateCameraInfoFromCurrentMatrices()
|
||||
clipToView,
|
||||
clipToPrevClip,
|
||||
prevClipToClip,
|
||||
worldToView,
|
||||
viewToWorld,
|
||||
cameraPos,
|
||||
cameraRight,
|
||||
cameraUp,
|
||||
@@ -9094,9 +9100,8 @@ static void QD3D12_RunUpscalerOrBlit(QD3D12Window& w)
|
||||
rrOptions.normalRoughnessMode = sl::DLSSDNormalRoughnessMode::ePacked;
|
||||
rrOptions.alphaUpscalingEnabled = sl::Boolean::eFalse;
|
||||
|
||||
Mat4 identity = Mat4::Identity();
|
||||
memcpy(&rrOptions.worldToCameraView, identity.m, sizeof(float) * 16);
|
||||
memcpy(&rrOptions.cameraViewToWorld, identity.m, sizeof(float) * 16);
|
||||
memcpy(&rrOptions.worldToCameraView, g_gl.cameraState.worldToView.m, sizeof(float) * 16);
|
||||
memcpy(&rrOptions.cameraViewToWorld, g_gl.cameraState.viewToWorld.m, sizeof(float) * 16);
|
||||
|
||||
const sl::Result rrOptionsResult = slDLSSDSetOptions(g_qd3d12Sl.viewport, rrOptions);
|
||||
if (rrOptionsResult != sl::Result::eOk)
|
||||
@@ -18521,6 +18526,8 @@ void QD3D12_SetCameraInfo(
|
||||
const float* clipToView,
|
||||
const float* clipToPrevClip,
|
||||
const float* prevClipToClip,
|
||||
const float* worldToView,
|
||||
const float* viewToWorld,
|
||||
const float* cameraPos,
|
||||
const float* cameraRight,
|
||||
const float* cameraUp,
|
||||
@@ -18542,6 +18549,12 @@ void QD3D12_SetCameraInfo(
|
||||
if (prevClipToClip)
|
||||
memcpy(g_gl.cameraState.prevClipToClip.m, prevClipToClip, sizeof(float) * 16);
|
||||
|
||||
if (worldToView)
|
||||
memcpy(g_gl.cameraState.worldToView.m, worldToView, sizeof(float) * 16);
|
||||
|
||||
if (viewToWorld)
|
||||
memcpy(g_gl.cameraState.viewToWorld.m, viewToWorld, sizeof(float) * 16);
|
||||
|
||||
if (cameraPos)
|
||||
{
|
||||
g_gl.cameraState.cameraPos[0] = cameraPos[0];
|
||||
|
||||
Reference in New Issue
Block a user