From 6f6d3e2a81f473c0aac90e82b66ea8ab17951a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 30 Sep 2026 12:21:11 -0600 Subject: [PATCH] Lighting: Take the specular viewer direction from the view matrix The viewer is at infinity along view space +z, so in world space, where lighting happens, it's the view matrix's third column rather than (0,0,1): turning the camera moves the highlights (gpu/lighting/specular). The shaders read it from u_view. In a Need for Speed Carbon frame replayed on a PSP, this and the pow bring the error of the cars from MSE 957 to 120 (Vulkan). Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Common/TransformCommon.cpp | 9 ++------- GPU/Common/TransformCommon.h | 7 ++++++- GPU/Common/VertexShaderGenerator.cpp | 12 ++++++++++-- GPU/Software/Lighting.cpp | 2 +- GPU/Software/Lighting.h | 1 + GPU/Software/TransformUnit.cpp | 2 ++ 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/GPU/Common/TransformCommon.cpp b/GPU/Common/TransformCommon.cpp index 4dbc15dcae..1c4d474090 100644 --- a/GPU/Common/TransformCommon.cpp +++ b/GPU/Common/TransformCommon.cpp @@ -41,7 +41,7 @@ Lighter::Lighter(int vertType) { materialSpecular.GetFromRGB(gstate.materialspecular); materialSpecular.a = 1.0f; specCoef_ = PSPSpecularCoef(getFloat24(gstate.materialspecularcoef)); - // viewer_ = Vec3f(-gstate.viewMatrix[9], -gstate.viewMatrix[10], -gstate.viewMatrix[11]); + viewDir_ = PSPViewDirection(gstate.viewMatrix); bool hasColor = (vertType & GE_VTYPE_COL_MASK) != 0; materialUpdate_ = hasColor ? (gstate.materialupdate & 7) : 0; @@ -148,13 +148,8 @@ void Lighter::Light(float colorOut0[4], float colorOut1[4], const float colorIn[ Color4 lightDiff(lcolor[1][l], 0.0f); Color4 diff = (lightDiff * *diffuse) * dot; - // Real PSP specular - static const Vec3f toViewer(0, 0, 1); - // Better specular - // Vec3f toViewer = (viewer - pos).NormalizedOr001(cpu_info.bSSE4_1); - if (doSpecular && facingLight) { - Vec3f halfVec = (toLight + toViewer).NormalizedOr001(cpu_info.bSSE4_1); + Vec3f halfVec = (toLight + viewDir_).NormalizedOr001(cpu_info.bSSE4_1); dot = Dot(halfVec, norm); if (dot > 0.0f) { diff --git a/GPU/Common/TransformCommon.h b/GPU/Common/TransformCommon.h index 6c314c3b7b..f82d9b1b86 100644 --- a/GPU/Common/TransformCommon.h +++ b/GPU/Common/TransformCommon.h @@ -85,6 +85,11 @@ inline float PSPSpecularCoef(float e) { return e; } +// The viewer is at infinity along view space +z, so in world space it's the view matrix's third column. +inline Vec3f PSPViewDirection(const float viewMatrix[12]) { + return Vec3f(viewMatrix[2], viewMatrix[5], viewMatrix[8]).NormalizedOr001(false); +} + // Convenient way to do precomputation to save the parts of the lighting calculation // that's common between the many vertices of a draw call. class Lighter { @@ -106,7 +111,7 @@ private: Color4 materialDiffuse; Color4 materialSpecular; float specCoef_; - // Vec3f viewer_; + Vec3f viewDir_; bool doShadeMapping_; int materialUpdate_; diff --git a/GPU/Common/VertexShaderGenerator.cpp b/GPU/Common/VertexShaderGenerator.cpp index 0c0a6ed0f9..07781220d6 100644 --- a/GPU/Common/VertexShaderGenerator.cpp +++ b/GPU/Common/VertexShaderGenerator.cpp @@ -500,6 +500,14 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag } else { WRITE(p, " mediump vec3 worldnormal = normalizeOr001(mul(vec4(0.0, 0.0, %s1.0, 0.0), u_world).xyz);\n", flipNormal ? "-" : ""); } + if (enableLighting) { + // The viewer is at infinity along view space +z: in world space, the view matrix's third column. + if (compat.shaderLanguage == HLSL_D3D11) { + WRITE(p, " mediump vec3 viewDir = normalizeOr001(vec3(u_view[0].z, u_view[1].z, u_view[2].z));\n"); + } else { + WRITE(p, " mediump vec3 viewDir = normalizeOr001(u_view[2].xyz);\n"); + } + } WRITE(p, " vec4 viewPos = vec4(mul(vec4(worldpos, 1.0), u_view).xyz, 1.0);\n"); if (useSimpleStereo) { @@ -668,7 +676,7 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag p.F(" diffuse = (u_lightdiffuse%s * diffuseColor) * max(ldot, 0.0);\n", iStr); p.C(" if (comp == 0x1u && ldot >= 0.0) {\n"); // do specular. note - must allow for the >= case, since the u_matspecular.a <= 0.0 case relies on it. p.C(" if (u_matspecular.a > 0.0) {\n"); - p.C(" vec3 halfVec = toLight + vec3(0.0, 0.0, 1.0);\n"); + p.C(" vec3 halfVec = toLight + viewDir;\n"); p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n"); p.C(" ldot = pspPow(dot(halfVec, worldnormal) * halfInvLen, u_matspecular.a);\n"); p.C(" } else {\n"); @@ -748,7 +756,7 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag if (doSpecular) { p.C(" if (ldot >= 0.0) {\n"); p.C(" if (u_matspecular.a > 0.0) {\n"); - p.C(" vec3 halfVec = toLight + vec3(0.0, 0.0, 1.0);\n"); + p.C(" vec3 halfVec = toLight + viewDir;\n"); p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n"); p.C(" ldot = pspPow(dot(halfVec, worldnormal) * halfInvLen, u_matspecular.a);\n"); p.C(" } else {\n"); diff --git a/GPU/Software/Lighting.cpp b/GPU/Software/Lighting.cpp index 040dd6b2fd..7696c7a6e5 100644 --- a/GPU/Software/Lighting.cpp +++ b/GPU/Software/Lighting.cpp @@ -369,7 +369,7 @@ static void ProcessSIMD(VertexData &vertex, const WorldCoords &worldpos, const W } if (lstate.specular && diffuse_factor >= 0.0f) { - Vec3 H = L + Vec3(0.f, 0.f, 1.f); + Vec3 H = L + state.viewDir; float specular_factor = Dot33(H.NormalizedOr001(useSSE4), worldnormal); specular_factor = pspLightPow(specular_factor, state.specularExp); diff --git a/GPU/Software/Lighting.h b/GPU/Software/Lighting.h index c875aae08b..2690ce9c62 100644 --- a/GPU/Software/Lighting.h +++ b/GPU/Software/Lighting.h @@ -53,6 +53,7 @@ struct State { Vec4 baseAmbientColorFactor; float specularExp; + Vec3f viewDir; struct { bool colorForAmbient : 1; diff --git a/GPU/Software/TransformUnit.cpp b/GPU/Software/TransformUnit.cpp index 13e53c7841..7cca12a646 100644 --- a/GPU/Software/TransformUnit.cpp +++ b/GPU/Software/TransformUnit.cpp @@ -28,6 +28,7 @@ #include "GPU/Common/DrawEngineCommon.h" #include "GPU/Common/VertexDecoderCommon.h" #include "GPU/Common/SoftwareTransformCommon.h" +#include "GPU/Common/TransformCommon.h" #include "GPU/Common/VertexReader.h" #include "GPU/GPUStateSIMDUtil.h" #include "Common/Math/SIMDHeaders.h" @@ -259,6 +260,7 @@ void ComputeTransformState(TransformState *state, const VertexReader &vreader) { } else { state->lightingState.usesWorldNormal = state->uvGenMode == GE_TEXMAP_ENVIRONMENT_MAP; } + state->lightingState.viewDir = PSPViewDirection(gstate.viewMatrix); float world[16]; float view[16];