diff --git a/resources/shaders/night_fog.vertex b/resources/shaders/night_fog.vertex new file mode 100644 index 0000000..f6890f0 --- /dev/null +++ b/resources/shaders/night_fog.vertex @@ -0,0 +1,22 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec3 vNormal; + +varying vec2 texCoord; +varying float fogDistance; +varying vec3 fragViewPos; +varying vec3 fragNormal; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 ModelViewMatrix; +uniform vec3 uPlayerEyePos; + +void main() +{ + vec4 eyePos = ModelViewMatrix * vec4(vPosition.xyz, 1.0); + fogDistance = length(eyePos.xyz - uPlayerEyePos); + gl_Position = ProjectionModelViewMatrix * vec4(vPosition.xyz, 1.0); + texCoord = vTexCoord; + fragViewPos = eyePos.xyz; + fragNormal = mat3(ModelViewMatrix) * vNormal; +} diff --git a/resources/shaders/night_fog_desktop.fragment b/resources/shaders/night_fog_desktop.fragment new file mode 100644 index 0000000..260d0ae --- /dev/null +++ b/resources/shaders/night_fog_desktop.fragment @@ -0,0 +1,56 @@ +uniform sampler2D Texture; +uniform float uAlpha; +uniform int uPointLightCount; +uniform vec3 uPointLightPos[4]; +uniform vec3 uPointLightDir[4]; +uniform vec3 uPointLightColor[4]; + +varying vec2 texCoord; +varying float fogDistance; +varying vec3 fragViewPos; +varying vec3 fragNormal; + +// cos(45 deg) = half-angle for a 90-degree full-angle spotlight +const float SPOT_COS_OUTER = 0.7071; +const float SPOT_COS_INNER = 0.82; // slightly tighter inner cone for smooth edge + +void main() +{ + vec4 color = texture2D(Texture, texCoord); + + if (color.a < 0.1) + discard; + + float ambient = 0.37; + vec3 lighting = vec3(ambient); + + bool hasNormal = dot(fragNormal, fragNormal) > 0.001; + vec3 N = hasNormal ? normalize(fragNormal) : vec3(0.0); + + for (int i = 0; i < 4; i++) + { + if (i >= uPointLightCount) break; + + vec3 lightVec = uPointLightPos[i] - fragViewPos; + float dist = length(lightVec); + vec3 lightDir = normalize(lightVec); + + // Spotlight cone: angle between the cone axis and the ray from light to fragment + float cosTheta = dot(-lightDir, normalize(uPointLightDir[i])); + float spotAtten = smoothstep(SPOT_COS_OUTER, SPOT_COS_INNER, cosTheta); + if (spotAtten <= 0.0) continue; + + float atten = 1.0 / (1.0 + 0.35 * dist + 0.15 * dist * dist); + float diff = hasNormal ? max(dot(N, lightDir), 0.0) : 1.0; + + lighting += uPointLightColor[i] * diff * atten * spotAtten; + } + + color.rgb *= lighting; + + vec3 fogColor = vec3(0.01, 0.01, 0.05); + float fogFactor = clamp((fogDistance - 15.0) / 8.0, 0.0, 1.0); + color.rgb = mix(color.rgb, fogColor, fogFactor); + + gl_FragColor = vec4(color.rgb, color.a * uAlpha); +} diff --git a/resources/shaders/night_fog_shadow.vertex b/resources/shaders/night_fog_shadow.vertex new file mode 100644 index 0000000..dc70ac9 --- /dev/null +++ b/resources/shaders/night_fog_shadow.vertex @@ -0,0 +1,25 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec3 vNormal; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; +varying float fogDistance; +varying vec3 fragViewPos; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 ModelViewMatrix; +uniform mat4 uLightFromCamera; +uniform vec3 uPlayerEyePos; + +void main() +{ + vec4 eyePos = ModelViewMatrix * vec4(vPosition, 1.0); + fogDistance = length(eyePos.xyz - uPlayerEyePos); + gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0); + texCoord = vTexCoord; + fragViewPos = eyePos.xyz; + fragNormal = mat3(ModelViewMatrix) * vNormal; + fragPosLightSpace = uLightFromCamera * eyePos; +} diff --git a/resources/shaders/night_fog_shadow_desktop.fragment b/resources/shaders/night_fog_shadow_desktop.fragment new file mode 100644 index 0000000..7f23b62 --- /dev/null +++ b/resources/shaders/night_fog_shadow_desktop.fragment @@ -0,0 +1,95 @@ +uniform sampler2D Texture; +uniform sampler2D uShadowMap; +uniform vec3 uLightDir; +uniform float uAlpha; +uniform int uPointLightCount; +uniform vec3 uPointLightPos[4]; +uniform vec3 uPointLightDir[4]; +uniform vec3 uPointLightColor[4]; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; +varying float fogDistance; +varying vec3 fragViewPos; + +const float SPOT_COS_OUTER = 0.7071; +const float SPOT_COS_INNER = 0.82; + +float computeShadow(vec4 lightSpacePos, vec3 normal) +{ + vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.x < 0.0 || projCoords.x > 1.0 || + projCoords.y < 0.0 || projCoords.y > 1.0 || + projCoords.z > 1.0) + { + return 0.0; + } + + float currentDepth = projCoords.z; + + float bias = 0.0004; + if (dot(normal, normal) > 0.001) + { + float cosTheta = dot(normalize(normal), -normalize(uLightDir)); + bias = max(0.002 * (1.0 - cosTheta), 0.0002); + } + + float shadow = 0.0; + float texelSize = 1.0 / 2048.0; + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float pcfDepth = texture2D(uShadowMap, projCoords.xy + vec2(float(x), float(y)) * texelSize).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; + + return shadow; +} + +void main() +{ + vec4 color = texture2D(Texture, texCoord); + + if (color.a < 0.1) + discard; + + float ambient = 0.37; + vec3 lighting = vec3(ambient); + + bool hasNormal = dot(fragNormal, fragNormal) > 0.001; + vec3 N = hasNormal ? normalize(fragNormal) : vec3(0.0); + + float shadow = computeShadow(fragPosLightSpace, fragNormal); + + for (int i = 0; i < 4; i++) + { + if (i >= uPointLightCount) break; + + vec3 lightVec = uPointLightPos[i] - fragViewPos; + float dist = length(lightVec); + vec3 lightDir = normalize(lightVec); + + float cosTheta = dot(-lightDir, normalize(uPointLightDir[i])); + float spotAtten = smoothstep(SPOT_COS_OUTER, SPOT_COS_INNER, cosTheta); + if (spotAtten <= 0.0) continue; + + float atten = 1.0 / (1.0 + 0.35 * dist + 0.15 * dist * dist); + float diff = hasNormal ? max(dot(N, lightDir), 0.0) : 1.0; + + lighting += uPointLightColor[i] * diff * atten * spotAtten * (1.0 - shadow); + } + + color.rgb *= lighting; + + vec3 fogColor = vec3(0.01, 0.01, 0.05); + float fogFactor = clamp((fogDistance - 15.0) / 8.0, 0.0, 1.0); + color.rgb = mix(color.rgb, fogColor, fogFactor); + + gl_FragColor = vec4(color.rgb, color.a * uAlpha); +} diff --git a/resources/shaders/night_fog_shadow_web.fragment b/resources/shaders/night_fog_shadow_web.fragment new file mode 100644 index 0000000..62b6b51 --- /dev/null +++ b/resources/shaders/night_fog_shadow_web.fragment @@ -0,0 +1,97 @@ +precision mediump float; + +uniform sampler2D Texture; +uniform sampler2D uShadowMap; +uniform vec3 uLightDir; +uniform float uAlpha; +uniform int uPointLightCount; +uniform vec3 uPointLightPos[4]; +uniform vec3 uPointLightDir[4]; +uniform vec3 uPointLightColor[4]; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; +varying float fogDistance; +varying vec3 fragViewPos; + +const float SPOT_COS_OUTER = 0.7071; +const float SPOT_COS_INNER = 0.82; + +float computeShadow(vec4 lightSpacePos, vec3 normal) +{ + vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.x < 0.0 || projCoords.x > 1.0 || + projCoords.y < 0.0 || projCoords.y > 1.0 || + projCoords.z > 1.0) + { + return 0.0; + } + + float currentDepth = projCoords.z; + + float bias = 0.0004; + if (dot(normal, normal) > 0.001) + { + float cosTheta = dot(normalize(normal), -normalize(uLightDir)); + bias = max(0.002 * (1.0 - cosTheta), 0.0002); + } + + float shadow = 0.0; + float texelSize = 1.0 / 2048.0; + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float pcfDepth = texture2D(uShadowMap, projCoords.xy + vec2(float(x), float(y)) * texelSize).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; + + return shadow; +} + +void main() +{ + vec4 color = texture2D(Texture, texCoord); + + if (color.a < 0.1) + discard; + + float ambient = 0.37; + vec3 lighting = vec3(ambient); + + bool hasNormal = dot(fragNormal, fragNormal) > 0.001; + vec3 N = hasNormal ? normalize(fragNormal) : vec3(0.0); + + float shadow = computeShadow(fragPosLightSpace, fragNormal); + + for (int i = 0; i < 4; i++) + { + if (i >= uPointLightCount) break; + + vec3 lightVec = uPointLightPos[i] - fragViewPos; + float dist = length(lightVec); + vec3 lightDir = normalize(lightVec); + + float cosTheta = dot(-lightDir, normalize(uPointLightDir[i])); + float spotAtten = smoothstep(SPOT_COS_OUTER, SPOT_COS_INNER, cosTheta); + if (spotAtten <= 0.0) continue; + + float atten = 1.0 / (1.0 + 0.35 * dist + 0.15 * dist * dist); + float diff = hasNormal ? max(dot(N, lightDir), 0.0) : 1.0; + + lighting += uPointLightColor[i] * diff * atten * spotAtten * (1.0 - shadow); + } + + color.rgb *= lighting; + + vec3 fogColor = vec3(0.01, 0.01, 0.05); + float fogFactor = clamp((fogDistance - 15.0) / 8.0, 0.0, 1.0); + color.rgb = mix(color.rgb, fogColor, fogFactor); + + gl_FragColor = vec4(color.rgb, color.a * uAlpha); +} diff --git a/resources/shaders/night_fog_skinning.vertex b/resources/shaders/night_fog_skinning.vertex new file mode 100644 index 0000000..cf99469 --- /dev/null +++ b/resources/shaders/night_fog_skinning.vertex @@ -0,0 +1,68 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec3 vNormal; +attribute vec4 aBoneIndices0; +attribute vec2 aBoneIndices1; +attribute vec4 aBoneWeights0; +attribute vec2 aBoneWeights1; + +varying vec2 texCoord; +varying float fogDistance; +varying vec3 fragViewPos; +varying vec3 fragNormal; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 ModelViewMatrix; +uniform mat4 uBoneMatrices[64]; +uniform vec3 uPlayerEyePos; + +void main() +{ + vec4 skinnedPos = vec4(0.0, 0.0, 0.0, 0.0); + vec3 skinnedNormal = vec3(0.0, 0.0, 0.0); + vec4 originalPos = vec4(vPosition, 1.0); + float totalWeight = 0.0; + + if (aBoneWeights0.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.x)] * originalPos * aBoneWeights0.x; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.x)]) * vNormal * aBoneWeights0.x; + totalWeight += aBoneWeights0.x; + } + if (aBoneWeights0.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.y)] * originalPos * aBoneWeights0.y; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.y)]) * vNormal * aBoneWeights0.y; + totalWeight += aBoneWeights0.y; + } + if (aBoneWeights0.z > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.z)] * originalPos * aBoneWeights0.z; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.z)]) * vNormal * aBoneWeights0.z; + totalWeight += aBoneWeights0.z; + } + if (aBoneWeights0.w > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.w)] * originalPos * aBoneWeights0.w; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.w)]) * vNormal * aBoneWeights0.w; + totalWeight += aBoneWeights0.w; + } + if (aBoneWeights1.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.x)] * originalPos * aBoneWeights1.x; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices1.x)]) * vNormal * aBoneWeights1.x; + totalWeight += aBoneWeights1.x; + } + if (aBoneWeights1.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.y)] * originalPos * aBoneWeights1.y; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices1.y)]) * vNormal * aBoneWeights1.y; + totalWeight += aBoneWeights1.y; + } + + if (totalWeight < 0.001) { + skinnedPos = originalPos; + skinnedNormal = vNormal; + } + + vec4 eyePos = ModelViewMatrix * skinnedPos; + fogDistance = length(eyePos.xyz - uPlayerEyePos); + gl_Position = ProjectionModelViewMatrix * skinnedPos; + texCoord = vTexCoord; + fragViewPos = eyePos.xyz; + fragNormal = mat3(ModelViewMatrix) * skinnedNormal; +} diff --git a/resources/shaders/night_fog_skinning_shadow.vertex b/resources/shaders/night_fog_skinning_shadow.vertex new file mode 100644 index 0000000..4ef4bc1 --- /dev/null +++ b/resources/shaders/night_fog_skinning_shadow.vertex @@ -0,0 +1,71 @@ +attribute vec3 vPosition; +attribute vec2 vTexCoord; +attribute vec3 vNormal; +attribute vec4 aBoneIndices0; +attribute vec2 aBoneIndices1; +attribute vec4 aBoneWeights0; +attribute vec2 aBoneWeights1; + +varying vec2 texCoord; +varying vec4 fragPosLightSpace; +varying vec3 fragNormal; +varying float fogDistance; +varying vec3 fragViewPos; + +uniform mat4 ProjectionModelViewMatrix; +uniform mat4 ModelViewMatrix; +uniform mat4 uLightFromCamera; +uniform mat4 uBoneMatrices[64]; +uniform vec3 uPlayerEyePos; + +void main() +{ + vec4 skinnedPos = vec4(0.0, 0.0, 0.0, 0.0); + vec3 skinnedNormal = vec3(0.0, 0.0, 0.0); + vec4 originalPos = vec4(vPosition, 1.0); + float totalWeight = 0.0; + + if (aBoneWeights0.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.x)] * originalPos * aBoneWeights0.x; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.x)]) * vNormal * aBoneWeights0.x; + totalWeight += aBoneWeights0.x; + } + if (aBoneWeights0.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.y)] * originalPos * aBoneWeights0.y; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.y)]) * vNormal * aBoneWeights0.y; + totalWeight += aBoneWeights0.y; + } + if (aBoneWeights0.z > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.z)] * originalPos * aBoneWeights0.z; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.z)]) * vNormal * aBoneWeights0.z; + totalWeight += aBoneWeights0.z; + } + if (aBoneWeights0.w > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices0.w)] * originalPos * aBoneWeights0.w; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices0.w)]) * vNormal * aBoneWeights0.w; + totalWeight += aBoneWeights0.w; + } + if (aBoneWeights1.x > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.x)] * originalPos * aBoneWeights1.x; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices1.x)]) * vNormal * aBoneWeights1.x; + totalWeight += aBoneWeights1.x; + } + if (aBoneWeights1.y > 0.0) { + skinnedPos += uBoneMatrices[int(aBoneIndices1.y)] * originalPos * aBoneWeights1.y; + skinnedNormal += mat3(uBoneMatrices[int(aBoneIndices1.y)]) * vNormal * aBoneWeights1.y; + totalWeight += aBoneWeights1.y; + } + + if (totalWeight < 0.001) { + skinnedPos = originalPos; + skinnedNormal = vNormal; + } + + vec4 eyePos = ModelViewMatrix * skinnedPos; + fogDistance = length(eyePos.xyz - uPlayerEyePos); + gl_Position = ProjectionModelViewMatrix * skinnedPos; + texCoord = vTexCoord; + fragViewPos = eyePos.xyz; + fragNormal = mat3(ModelViewMatrix) * skinnedNormal; + fragPosLightSpace = uLightFromCamera * eyePos; +} diff --git a/resources/shaders/night_fog_web.fragment b/resources/shaders/night_fog_web.fragment new file mode 100644 index 0000000..bf8c06a --- /dev/null +++ b/resources/shaders/night_fog_web.fragment @@ -0,0 +1,58 @@ +precision mediump float; + +uniform sampler2D Texture; +uniform float uAlpha; +uniform int uPointLightCount; +uniform vec3 uPointLightPos[4]; +uniform vec3 uPointLightDir[4]; +uniform vec3 uPointLightColor[4]; + +varying vec2 texCoord; +varying float fogDistance; +varying vec3 fragViewPos; +varying vec3 fragNormal; + +// cos(45 deg) = half-angle for a 90-degree full-angle spotlight +const float SPOT_COS_OUTER = 0.7071; +const float SPOT_COS_INNER = 0.82; // slightly tighter inner cone for smooth edge + +void main() +{ + vec4 color = texture2D(Texture, texCoord); + + if (color.a < 0.1) + discard; + + float ambient = 0.37; + vec3 lighting = vec3(ambient); + + bool hasNormal = dot(fragNormal, fragNormal) > 0.001; + vec3 N = hasNormal ? normalize(fragNormal) : vec3(0.0); + + for (int i = 0; i < 4; i++) + { + if (i >= uPointLightCount) break; + + vec3 lightVec = uPointLightPos[i] - fragViewPos; + float dist = length(lightVec); + vec3 lightDir = normalize(lightVec); + + // Spotlight cone: angle between the cone axis and the ray from light to fragment + float cosTheta = dot(-lightDir, normalize(uPointLightDir[i])); + float spotAtten = smoothstep(SPOT_COS_OUTER, SPOT_COS_INNER, cosTheta); + if (spotAtten <= 0.0) continue; + + float atten = 1.0 / (1.0 + 0.35 * dist + 0.15 * dist * dist); + float diff = hasNormal ? max(dot(N, lightDir), 0.0) : 1.0; + + lighting += uPointLightColor[i] * diff * atten * spotAtten; + } + + color.rgb *= lighting; + + vec3 fogColor = vec3(0.01, 0.01, 0.05); + float fogFactor = clamp((fogDistance - 15.0) / 8.0, 0.0, 1.0); + color.rgb = mix(color.rgb, fogColor, fogFactor); + + gl_FragColor = vec4(color.rgb, color.a * uAlpha); +} diff --git a/src/Character.cpp b/src/Character.cpp index 6093d9c..5abaefc 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -901,6 +901,295 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri CheckGlError(__FILE__, __LINE__); } +// ==================== Night pass with point lights ==================== + +void Character::drawNight(Renderer& renderer, const float* pointLightPosEye, const float* pointLightDirEye, const float* pointLightColors, int pointLightCount) { + if (!enabled) return; + if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { + hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); + } + + renderer.RenderUniform1f("uAlpha", 1.0f); + glDisable(GL_BLEND); + + if (!isPlayer && hp <= 0) return; + + if (useGpuSkinning) { + drawNightGpuSkinning(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount); + } else { + drawNightCpu(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount); + } +} + +void Character::drawNightGpuSkinning(Renderer& renderer, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount) { + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end()) return; + + if (!prepareGpuSkinning()) return; + + static const std::string nightSkinningShader = "night_fog_skinning"; + static const std::string boneMatricesUniform = "uBoneMatrices[0]"; + + renderer.shaderManager.PushShader(nightSkinningShader); + renderer.RenderUniform1i(textureUniformName, 0); + const float playerEyePosNight[3] = { 0.0f, 0.0f, -Environment::zoom }; + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); + renderer.RenderUniform1i("uPointLightCount", plCount); + if (plCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); + } + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, + static_cast(it->second.gpuSkinningShaderData.skinningMatrices.size()), false, + it->second.gpuSkinningShaderData.skinningMatrices[0].data()); + + for (const auto& name : it->second.model.meshNamesOrdered) { + auto pit = it->second.gpuSkinningShaderData.perMesh.find(name); + if (pit == it->second.gpuSkinningShaderData.perMesh.end()) continue; + auto tit = meshTextures.find(name); + if (tit == meshTextures.end() || !tit->second) continue; + glBindTexture(GL_TEXTURE_2D, tit->second->getTexID()); + pit->second.RenderVBO(renderer); + } + + renderer.PopMatrix(); + + renderer.shaderManager.PushShader("night_fog"); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); + renderer.RenderUniform1i("uPointLightCount", plCount); + if (plCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); + } + drawAttachedWeapon(renderer); + renderer.shaderManager.PopShader(); + + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + +void Character::drawNightCpu(Renderer& renderer, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount) { + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end()) return; + + static const std::string nightShader = "night_fog"; + + renderer.shaderManager.PushShader(nightShader); + renderer.RenderUniform1i(textureUniformName, 0); + const float playerEyePosNight[3] = { 0.0f, 0.0f, -Environment::zoom }; + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); + renderer.RenderUniform1i("uPointLightCount", plCount); + if (plCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); + } + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + auto& anim = it->second; + for (const auto& name : anim.model.meshNamesOrdered) { + auto mit = anim.model.meshes.find(name); + if (mit == anim.model.meshes.end()) continue; + auto tit = meshTextures.find(name); + if (tit == meshTextures.end() || !tit->second) continue; + glBindTexture(GL_TEXTURE_2D, tit->second->getTexID()); + modelMutable.AssignFrom(mit->second.mesh); + modelMutable.RefreshVBO(); + renderer.DrawVertexRenderStruct(modelMutable); + } + + renderer.PopMatrix(); + + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); + drawAttachedWeapon(renderer); + + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + +// ==================== Night pass with spot-light + shadow ==================== + +void Character::drawNightWithShadow(Renderer& renderer, + const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, + const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex) +{ + if (!enabled) return; + if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { + hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); + } + renderer.RenderUniform1f("uAlpha", 1.0f); + glDisable(GL_BLEND); + if (!isPlayer && hp <= 0) return; + + if (useGpuSkinning) { + drawNightGpuSkinningWithShadow(renderer, plPosEye, plDirEye, plColors, plCount, lightFromCamera, shadowMapTex); + } else { + drawNightCpuWithShadow(renderer, plPosEye, plDirEye, plColors, plCount, lightFromCamera, shadowMapTex); + } +} + +void Character::drawNightGpuSkinningWithShadow(Renderer& renderer, + const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, + const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex) +{ + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end()) return; + if (!prepareGpuSkinning()) return; + + static const std::string nightShadowSkinningShader = "night_fog_skinning_shadow"; + static const std::string boneMatricesUniform = "uBoneMatrices[0]"; + + renderer.shaderManager.PushShader(nightShadowSkinningShader); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1i("uShadowMap", 1); + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + // First light's eye-space direction used for shadow bias + if (plCount > 0) renderer.RenderUniform3fv("uLightDir", plDirEye); + + const float playerEyePosNightShadow[3] = { 0.0f, 0.0f, -Environment::zoom }; + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); + renderer.RenderUniform1i("uPointLightCount", plCount); + if (plCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); + } + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shadowMapTex); + glActiveTexture(GL_TEXTURE0); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, + static_cast(it->second.gpuSkinningShaderData.skinningMatrices.size()), false, + it->second.gpuSkinningShaderData.skinningMatrices[0].data()); + + for (const auto& name : it->second.model.meshNamesOrdered) { + auto pit = it->second.gpuSkinningShaderData.perMesh.find(name); + if (pit == it->second.gpuSkinningShaderData.perMesh.end()) continue; + auto tit = meshTextures.find(name); + if (tit == meshTextures.end() || !tit->second) continue; + glBindTexture(GL_TEXTURE_2D, tit->second->getTexID()); + pit->second.RenderVBO(renderer); + } + + renderer.PopMatrix(); + + // Weapon uses the non-skinning night shadow shader + renderer.shaderManager.PushShader("night_fog_shadow"); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1i("uShadowMap", 1); + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + if (plCount > 0) renderer.RenderUniform3fv("uLightDir", plDirEye); + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); + renderer.RenderUniform1i("uPointLightCount", plCount); + if (plCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); + } + drawAttachedWeapon(renderer); + renderer.shaderManager.PopShader(); + + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + +void Character::drawNightCpuWithShadow(Renderer& renderer, + const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, + const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex) +{ + AnimationState drawState = resolveActiveState(); + auto it = animations.find(drawState); + if (it == animations.end()) return; + + static const std::string nightShadowShader = "night_fog_shadow"; + + renderer.shaderManager.PushShader(nightShadowShader); + renderer.RenderUniform1i(textureUniformName, 0); + renderer.RenderUniform1i("uShadowMap", 1); + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + if (plCount > 0) renderer.RenderUniform3fv("uLightDir", plDirEye); + + const float playerEyePosNightShadow[3] = { 0.0f, 0.0f, -Environment::zoom }; + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); + renderer.RenderUniform1i("uPointLightCount", plCount); + if (plCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); + } + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shadowMapTex); + glActiveTexture(GL_TEXTURE0); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + renderer.PushMatrix(); + renderer.TranslateMatrix({ position.x(), position.y(), position.z() }); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.ScaleMatrix(modelScale); + renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix()); + + auto& anim = it->second; + for (const auto& name : anim.model.meshNamesOrdered) { + auto mit = anim.model.meshes.find(name); + if (mit == anim.model.meshes.end()) continue; + auto tit = meshTextures.find(name); + if (tit == meshTextures.end() || !tit->second) continue; + glBindTexture(GL_TEXTURE_2D, tit->second->getTexID()); + modelMutable.AssignFrom(mit->second.mesh); + modelMutable.RefreshVBO(); + renderer.DrawVertexRenderStruct(modelMutable); + } + + renderer.PopMatrix(); + + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); + drawAttachedWeapon(renderer); + + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + void Character::setupHitSparks(std::shared_ptr sparkTexture) { hitSparkEmitter.setTexture(sparkTexture); diff --git a/src/Character.h b/src/Character.h index fcff55c..685614d 100644 --- a/src/Character.h +++ b/src/Character.h @@ -51,6 +51,10 @@ public: void draw(Renderer& renderer); void drawShadowDepth(Renderer& renderer); void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); + void drawNight(Renderer& renderer, const float* pointLightPosEye, const float* pointLightDirEye, const float* pointLightColors, int pointLightCount); + void drawNightWithShadow(Renderer& renderer, + const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, + const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex); // Character-to-character collision (XZ-plane). Used by Location to keep // player/NPCs from walking through each other. @@ -201,6 +205,16 @@ private: // Shadow: draw with shadow-aware shaders void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); void drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); + // Night: draw with spot-light shaders + void drawNightGpuSkinning(Renderer& renderer, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount); + void drawNightCpu(Renderer& renderer, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount); + // Night: draw with spot-light + shadow shaders + void drawNightGpuSkinningWithShadow(Renderer& renderer, + const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, + const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex); + void drawNightCpuWithShadow(Renderer& renderer, + const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, + const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex); }; } // namespace ZL diff --git a/src/Game.cpp b/src/Game.cpp index 79c986b..c892d2b 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -159,6 +159,10 @@ namespace ZL renderer.shaderManager.AddShaderFromFiles("fog_shadow", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE); std::cout << "Load resurces step 12x5" << std::endl; renderer.shaderManager.AddShaderFromFiles("fog_skinning_shadow", "resources/shaders/fog_skinning_shadow.vertex", "resources/shaders/fog_shadow_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog", "resources/shaders/night_fog.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog_skinning", "resources/shaders/night_fog_skinning.vertex", "resources/shaders/night_fog_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog_shadow", "resources/shaders/night_fog_shadow.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog_skinning_shadow", "resources/shaders/night_fog_skinning_shadow.vertex", "resources/shaders/night_fog_shadow_web.fragment", CONST_ZIP_FILE); #else renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_desktop.fragment", CONST_ZIP_FILE); @@ -181,6 +185,10 @@ namespace ZL renderer.shaderManager.AddShaderFromFiles("skinning_shadow", "resources/shaders/skinning_shadow.vertex", "resources/shaders/default_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_shadow", "resources/shaders/fog_shadow.vertex", "resources/shaders/fog_shadow_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("fog_skinning_shadow", "resources/shaders/fog_skinning_shadow.vertex", "resources/shaders/fog_shadow_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog", "resources/shaders/night_fog.vertex", "resources/shaders/night_fog_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog_skinning", "resources/shaders/night_fog_skinning.vertex", "resources/shaders/night_fog_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog_shadow", "resources/shaders/night_fog_shadow.vertex", "resources/shaders/night_fog_shadow_desktop.fragment", CONST_ZIP_FILE); + renderer.shaderManager.AddShaderFromFiles("night_fog_skinning_shadow", "resources/shaders/night_fog_skinning_shadow.vertex", "resources/shaders/night_fog_shadow_desktop.fragment", CONST_ZIP_FILE); #endif @@ -414,6 +422,18 @@ namespace ZL }); } + // Add test spotlight to every location for night-mode testing. + { + PointLight testLight; + //testLight.position = Eigen::Vector3f(-3.5951f, 3.0f, 0.280929f); + testLight.position = Eigen::Vector3f(-3.5951f, 6.0f, 0.280929f); + testLight.direction = Eigen::Vector3f(0.0f, -1.0f, 0.0f); // straight down + //testLight.color = Eigen::Vector3f(2.0f, 1.6f, 1.2f); + testLight.color = Eigen::Vector3f(4.0f, 3.2f, 2.4f); + for (auto& [name, loc] : locations) + loc->pointLights.push_back(testLight); + } + currentLocation = locations["location_dorm"]; currentLocation->scriptEngine.callLocationEnterCallback(); @@ -520,13 +540,18 @@ namespace ZL { if (currentLocation) { - // Sync global darklands flag so Location's raycast and draw functions see it. + // Sync global flags so Location's draw functions see them. currentLocation->isDarklands = isDarklands; + currentLocation->isNight = isNight; if (isDarklands) { currentLocation->drawGameDarklands(); CheckGlError(__FILE__, __LINE__); } + else if (isNight) { + currentLocation->drawGameNight(); + CheckGlError(__FILE__, __LINE__); + } else if (currentLocation->shadowMap) { CheckGlError(__FILE__, __LINE__); currentLocation->drawShadowDepthPass(); @@ -727,6 +752,13 @@ namespace ZL if (event.type == SDL_KEYDOWN && event.key.repeat == 0) { switch (event.key.keysym.sym) { + case SDLK_9: + if (editorMode == EditorMode::InteractiveObjects && currentLocation) { + currentLocation->editor.selectInteractiveObject(9); + } else { + isNight = !isNight; + } + break; case SDLK_0: case SDLK_1: case SDLK_2: @@ -736,7 +768,6 @@ namespace ZL case SDLK_6: case SDLK_7: case SDLK_8: - case SDLK_9: if (editorMode == EditorMode::InteractiveObjects && currentLocation) { currentLocation->editor.selectInteractiveObject(event.key.keysym.sym - SDLK_0); } else if (event.key.keysym.sym == SDLK_0) { diff --git a/src/Game.h b/src/Game.h index 5e7dc84..5a0a6a8 100644 --- a/src/Game.h +++ b/src/Game.h @@ -52,6 +52,9 @@ namespace ZL { // Returns false if a transition is already in progress. bool startDarklandsTransition(); + // Global night mode state — persists across location transitions. + bool isNight = false; + Inventory inventory; InteractiveObject* pickedUpObject = nullptr; diff --git a/src/Location.cpp b/src/Location.cpp index 2072a8f..482c006 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "GameConstants.h" #include "Character.h" #include "external/nlohmann/json.hpp" @@ -878,6 +879,185 @@ namespace ZL } } + void Location::drawNightShadowDepthPass(const PointLight& light) + { + if (!shadowMap) return; + static constexpr float kSpotFovY = static_cast(M_PI) * 0.5f; // 90° + shadowMap->updateSpotlightMatrix(light.position, light.direction, kSpotFovY, 0.1f, 20.0f); + shadowMap->bind(); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glEnable(GL_CULL_FACE); + + renderer.shaderManager.PushShader("shadow_depth"); + renderer.PushPerspectiveProjectionMatrix(kSpotFovY, 1.0f, 0.1f, 20.0f); + renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix()); + + for (auto& [name, gameObj] : gameObjects) { + renderer.DrawVertexRenderStruct(gameObj.mesh); + } + for (auto& intObj : interactiveObjects) { + if (intObj.isActive && intObj.loadedObject.texture) { + renderer.PushMatrix(); + renderer.TranslateMatrix(intObj.position); + renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh); + renderer.PopMatrix(); + } + } + if (player) player->drawShadowDepth(renderer); + for (auto& npc : npcs) npc->drawShadowDepth(renderer); + + renderer.PopMatrix(); // light view + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + glDisable(GL_CULL_FACE); + shadowMap->unbind(); + } + + void Location::drawGameNight() + { + // --- Find nearest spotlight for shadow casting --- + const PointLight* shadowLight = nullptr; + if (player && !pointLights.empty()) { + float minDist = FLT_MAX; + for (const auto& pl : pointLights) { + const float d = (pl.position - player->position).norm(); + if (d < minDist) { minDist = d; shadowLight = &pl; } + } + } + const bool hasShadows = (shadowMap != nullptr && shadowLight != nullptr); + + if (hasShadows) { + drawNightShadowDepthPass(*shadowLight); + } + + // --- Main render pass --- + glClearColor(0.01f, 0.01f, 0.05f, 1.0f); + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + + const std::string mainShader = hasShadows ? "night_fog_shadow" : "night_fog"; + renderer.shaderManager.PushShader(mainShader); + renderer.RenderUniform1i(textureUniformName, 0); + + if (hasShadows) { + renderer.RenderUniform1i("uShadowMap", 1); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture()); + glActiveTexture(GL_TEXTURE0); + } + + const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom }; + renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos); + + renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + renderer.PushMatrix(); + + renderer.LoadIdentity(); + renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom }); + + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); + const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero(); + renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); + + cameraViewMatrix = renderer.GetCurrentModelViewMatrix(); + + // Transform spot lights to eye space and build uniform arrays (max 4) + static constexpr int MAX_POINT_LIGHTS = 4; + float plPosEye[MAX_POINT_LIGHTS * 3] = {}; + float plDirEye[MAX_POINT_LIGHTS * 3] = {}; + float plColors[MAX_POINT_LIGHTS * 3] = {}; + const Eigen::Matrix3f camRot = cameraViewMatrix.block<3, 3>(0, 0); + const int lightCount = min(static_cast(pointLights.size()), MAX_POINT_LIGHTS); + for (int i = 0; i < lightCount; ++i) { + const Eigen::Vector4f worldPos(pointLights[i].position.x(), pointLights[i].position.y(), pointLights[i].position.z(), 1.0f); + const Eigen::Vector4f eyePos4 = cameraViewMatrix * worldPos; + plPosEye[i * 3 + 0] = eyePos4.x(); + plPosEye[i * 3 + 1] = eyePos4.y(); + plPosEye[i * 3 + 2] = eyePos4.z(); + const Eigen::Vector3f eyeDir = camRot * pointLights[i].direction; + plDirEye[i * 3 + 0] = eyeDir.x(); + plDirEye[i * 3 + 1] = eyeDir.y(); + plDirEye[i * 3 + 2] = eyeDir.z(); + plColors[i * 3 + 0] = pointLights[i].color.x(); + plColors[i * 3 + 1] = pointLights[i].color.y(); + plColors[i * 3 + 2] = pointLights[i].color.z(); + } + + // Compute light-from-camera matrix for shadow lookup + Eigen::Matrix4f lightFromCamera = Eigen::Matrix4f::Identity(); + if (hasShadows) { + lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewMatrix.inverse(); + renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); + // Pass first light's eye-space direction as uLightDir for shadow bias + renderer.RenderUniform3fv("uLightDir", plDirEye); + } + + renderer.RenderUniform1i("uPointLightCount", lightCount); + if (lightCount > 0) { + renderer.RenderUniform3fvArray("uPointLightPos[0]", lightCount, plPosEye); + renderer.RenderUniform3fvArray("uPointLightDir[0]", lightCount, plDirEye); + renderer.RenderUniform3fvArray("uPointLightColor[0]", lightCount, plColors); + } + + renderer.RenderUniform1f("uAlpha", 1.0f); + for (auto& [name, gameObj] : gameObjects) { + if (!gameObj.texture) continue; + glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID()); + renderer.DrawVertexRenderStruct(gameObj.mesh); + } + + for (auto& intObj : interactiveObjects) { + if (intObj.isActive) { + intObj.draw(renderer); + } + } + + renderer.RenderUniform1f("uAlpha", 1.0f); + if (player) player->prepareHitSparksForDraw(cameraViewMatrix); + for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix); + for (auto& tz : teleportZones) tz.prepareForDraw(cameraViewMatrix); + + if (hasShadows) { + if (player) player->drawNightWithShadow(renderer, plPosEye, plDirEye, plColors, lightCount, lightFromCamera, shadowMap->getDepthTexture()); + for (auto& npc : npcs) npc->drawNightWithShadow(renderer, plPosEye, plDirEye, plColors, lightCount, lightFromCamera, shadowMap->getDepthTexture()); + } else { + if (player) player->drawNight(renderer, plPosEye, plDirEye, plColors, lightCount); + for (auto& npc : npcs) npc->drawNight(renderer, plPosEye, plDirEye, plColors, lightCount); + } + + for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); + + if (editorMode == EditorMode::Navigation) { + editor.drawNavigation(); + editor.drawPoints(); + } + if (editorMode == EditorMode::InteractiveObjects) { + editor.drawInteractiveObjectBounds(); + } + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + if (npcNameText) { + Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + for (auto& npc : npcs) { + if (npc) npc->drawName(*npcNameText, cameraViewMatrix, proj); + } + if (player) player->drawHealthBar(renderer, cameraViewMatrix, proj); + for (auto& npc : npcs) { + if (npc) npc->drawHealthBar(renderer, cameraViewMatrix, proj); + } + } + } + bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available) { if (!navigation) return false; diff --git a/src/Location.h b/src/Location.h index 531b594..f056c5d 100644 --- a/src/Location.h +++ b/src/Location.h @@ -20,6 +20,13 @@ namespace ZL { + struct PointLight + { + Eigen::Vector3f position; // world space + Eigen::Vector3f direction; // world space, unit vector the cone points toward + Eigen::Vector3f color; // RGB, values > 1.0 increase intensity + }; + struct TriggerZone { std::string id; @@ -83,9 +90,12 @@ namespace ZL std::vector triggerZones; + std::vector pointLights; + // Set by Game and kept in sync across location transitions. // Read by draw functions and raycast — do not write from Location code. bool isDarklands = false; + bool isNight = false; // Called when the player successfully taps the ground and a floor walk target is set. // Used by the tutorial system to detect the "tap to walk" gesture. @@ -122,6 +132,8 @@ namespace ZL void drawShadowDepthPass(); void drawGameWithShadows(); void drawGameDarklands(); + void drawGameNight(); + void drawNightShadowDepthPass(const PointLight& light); bool setNavigationAreaAvailable(const std::string& areaName, bool available); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index cb8919c..1c91da0 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -825,6 +825,16 @@ namespace ZL { } } + void Renderer::RenderUniform3fvArray(const std::string& uniformName, int count, const float* value) + { + auto shader = shaderManager.GetCurrentShader(); + auto uniform = shader->uniformList.find(uniformName); + if (uniform != shader->uniformList.end()) + { + glUniform3fv(uniform->second, count, value); + } + } + void Renderer::RenderUniform4fv(const std::string& uniformName, const float* value) { auto shader = shaderManager.GetCurrentShader(); diff --git a/src/render/Renderer.h b/src/render/Renderer.h index 51614fc..35d7941 100644 --- a/src/render/Renderer.h +++ b/src/render/Renderer.h @@ -135,6 +135,7 @@ namespace ZL { void RenderUniformMatrix4fvArray(const std::string& uniformName, int count, bool transpose, const float* value); void RenderUniform1i(const std::string& uniformName, const int value); void RenderUniform3fv(const std::string& uniformName, const float* value); + void RenderUniform3fvArray(const std::string& uniformName, int count, const float* value); void RenderUniform4fv(const std::string& uniformName, const float* value); void RenderUniform1f(const std::string& uniformName, float value); diff --git a/src/render/ShadowMap.cpp b/src/render/ShadowMap.cpp index 55e3503..e437722 100644 --- a/src/render/ShadowMap.cpp +++ b/src/render/ShadowMap.cpp @@ -133,6 +133,29 @@ namespace ZL { lightSpaceMatrix = lightProjectionMatrix * lightViewMatrix; } + static Eigen::Matrix4f perspective(float fovY, float aspect, float zNear, float zFar) { + float t = std::tan(fovY * 0.5f); + Eigen::Matrix4f m = Eigen::Matrix4f::Zero(); + m(0, 0) = 1.0f / (aspect * t); + m(1, 1) = 1.0f / t; + m(2, 2) = -(zFar + zNear) / (zFar - zNear); + m(2, 3) = -(2.0f * zFar * zNear) / (zFar - zNear); + m(3, 2) = -1.0f; + return m; + } + + void ShadowMap::updateSpotlightMatrix(const Eigen::Vector3f& lightPos, + const Eigen::Vector3f& lightDir, + float fovY, float nearZ, float farZ) { + Eigen::Vector3f d = lightDir.normalized(); + Eigen::Vector3f up(0.f, 1.f, 0.f); + if (std::abs(d.dot(up)) > 0.99f) + up = Eigen::Vector3f(1.f, 0.f, 0.f); + lightViewMatrix = lookAt(lightPos, lightPos + d, up); + lightProjectionMatrix = perspective(fovY, 1.0f, nearZ, farZ); + lightSpaceMatrix = lightProjectionMatrix * lightViewMatrix; + } + void ShadowMap::bind() { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glViewport(0, 0, resolution, resolution); diff --git a/src/render/ShadowMap.h b/src/render/ShadowMap.h index 11b6d67..1a10d9c 100644 --- a/src/render/ShadowMap.h +++ b/src/render/ShadowMap.h @@ -29,6 +29,9 @@ namespace ZL { void setLightDirection(const Eigen::Vector3f& dir); void updateLightSpaceMatrix(const Eigen::Vector3f& sceneCenter); + void updateSpotlightMatrix(const Eigen::Vector3f& lightPos, + const Eigen::Vector3f& lightDir, + float fovY, float nearZ, float farZ); void bind(); void unbind();