From 27045a3a6a91ada291eadee8c8734e432ce9c153 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 5 Jan 2026 09:55:38 +0000 Subject: [PATCH] feat: Implement PBR shaders and enhance atmospheric effects in rendering --- CMakeUserPresets.json | 3 +- config/seed_runtime.json | 12 +++ scripts/cube_logic.lua | 2 +- shaders/pbr.frag | 169 ++++++++++++++++++++++++++++++++++++ shaders/pbr.frag.spv | Bin 0 -> 11932 bytes shaders/pbr.vert | 29 +++++++ shaders/pbr.vert.spv | Bin 0 -> 2476 bytes shaders/shadow.frag | 6 ++ shaders/shadow.frag.spv | Bin 0 -> 180 bytes shaders/shadow.vert | 14 +++ shaders/shadow.vert.spv | Bin 0 -> 1388 bytes shaders/ssgi.frag | 65 ++++++++++++++ shaders/ssgi.frag.spv | Bin 0 -> 4644 bytes shaders/volumetric.frag | 46 ++++++++++ shaders/volumetric.frag.spv | Bin 0 -> 2732 bytes 15 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 shaders/pbr.frag create mode 100644 shaders/pbr.frag.spv create mode 100644 shaders/pbr.vert create mode 100644 shaders/pbr.vert.spv create mode 100644 shaders/shadow.frag create mode 100644 shaders/shadow.frag.spv create mode 100644 shaders/shadow.vert create mode 100644 shaders/shadow.vert.spv create mode 100644 shaders/ssgi.frag create mode 100644 shaders/ssgi.frag.spv create mode 100644 shaders/volumetric.frag create mode 100644 shaders/volumetric.frag.spv diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json index 889fff9..e2739a5 100644 --- a/CMakeUserPresets.json +++ b/CMakeUserPresets.json @@ -4,6 +4,7 @@ "conan": {} }, "include": [ - "build/build/Release/generators/CMakePresets.json" + "build/build/Release/generators/CMakePresets.json", + "build/Release/generators/CMakePresets.json" ] } \ No newline at end of file diff --git a/config/seed_runtime.json b/config/seed_runtime.json index eaaa67c..a3159c6 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -60,5 +60,17 @@ "device_extensions": [ "VK_KHR_swapchain" ], + "atmospherics": { + "ambient_strength": 0.01, + "fog_density": 0.003, + "fog_color": [0.05, 0.05, 0.08], + "gamma": 2.2, + "enable_tone_mapping": true, + "enable_shadows": true, + "enable_ssgi": true, + "enable_volumetric_lighting": true, + "pbr_roughness": 0.3, + "pbr_metallic": 0.1 + }, "config_file": "config/seed_runtime.json" } diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 590cc34..52ab6f9 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -538,7 +538,7 @@ local function create_static_cube(position, scale, color) vertices = vertices, indices = cube_indices, compute_model_matrix = compute_model_matrix, - shader_key = "solid", -- Use solid color shader for room objects + shader_key = "pbr", -- Use PBR shader for realistic materials and lighting } end diff --git a/shaders/pbr.frag b/shaders/pbr.frag new file mode 100644 index 0000000..53a3391 --- /dev/null +++ b/shaders/pbr.frag @@ -0,0 +1,169 @@ +#version 450 + +layout(location = 0) in vec3 fragColor; +layout(location = 1) in vec3 fragWorldPos; +layout(location = 2) in vec3 fragNormal; +layout(location = 3) in vec2 fragTexCoord; + +layout(location = 0) out vec4 outColor; + +// Material properties +layout(push_constant) uniform PushConstants { + mat4 model; + mat4 view; + mat4 proj; + mat4 lightViewProj; + vec3 cameraPos; + float time; +} pc; + +// Lighting uniforms +layout(set = 0, binding = 0) uniform sampler2D shadowMap; + +// Material parameters (can be extended to use textures) +const vec3 MATERIAL_ALBEDO = vec3(0.8, 0.8, 0.8); +const float MATERIAL_ROUGHNESS = 0.3; +const float MATERIAL_METALLIC = 0.1; + +// Atmospheric parameters +const vec3 FOG_COLOR = vec3(0.05, 0.05, 0.08); +const float FOG_DENSITY = 0.003; +const float AMBIENT_STRENGTH = 0.01; // Much darker ambient + +// Light properties +const vec3 LIGHT_COLOR = vec3(1.0, 0.9, 0.6); +const float LIGHT_INTENSITY = 1.2; +const vec3 LIGHT_POSITIONS[8] = vec3[8]( + vec3(13.0, 4.5, 13.0), + vec3(-13.0, 4.5, 13.0), + vec3(13.0, 4.5, -13.0), + vec3(-13.0, 4.5, -13.0), + vec3(0.0, 4.5, 13.0), + vec3(0.0, 4.5, -13.0), + vec3(13.0, 4.5, 0.0), + vec3(-13.0, 4.5, 0.0) +); + +// PBR functions +vec3 fresnelSchlick(float cosTheta, vec3 F0) { + return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +float DistributionGGX(vec3 N, vec3 H, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH * NdotH; + + float num = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = 3.14159 * denom * denom; + + return num / denom; +} + +float GeometrySchlickGGX(float NdotV, float roughness) { + float r = (roughness + 1.0); + float k = (r * r) / 8.0; + + float num = NdotV; + float denom = NdotV * (1.0 - k) + k; + + return num / denom; +} + +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = GeometrySchlickGGX(NdotV, roughness); + float ggx1 = GeometrySchlickGGX(NdotL, roughness); + + return ggx1 * ggx2; +} + +float calculateShadow(vec3 worldPos) { + vec4 lightSpacePos = pc.lightViewProj * vec4(worldPos, 1.0); + vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.z > 1.0) return 0.0; + + float closestDepth = texture(shadowMap, projCoords.xy).r; + float currentDepth = projCoords.z; + + float shadow = 0.0; + vec2 texelSize = 1.0 / textureSize(shadowMap, 0); + for(int x = -1; x <= 1; ++x) { + for(int y = -1; y <= 1; ++y) { + float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; + shadow += currentDepth - 0.005 > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; + + return shadow; +} + +vec3 calculateLighting(vec3 worldPos, vec3 normal, vec3 viewDir) { + vec3 F0 = vec3(0.04); + F0 = mix(F0, MATERIAL_ALBEDO, MATERIAL_METALLIC); + + vec3 Lo = vec3(0.0); + + for (int i = 0; i < 8; i++) { + vec3 lightDir = normalize(LIGHT_POSITIONS[i] - worldPos); + vec3 halfway = normalize(viewDir + lightDir); + + float distance = length(LIGHT_POSITIONS[i] - worldPos); + float attenuation = 1.0 / (distance * distance); + vec3 radiance = LIGHT_COLOR * LIGHT_INTENSITY * attenuation; + + float NDF = DistributionGGX(normal, halfway, MATERIAL_ROUGHNESS); + float G = GeometrySmith(normal, viewDir, lightDir, MATERIAL_ROUGHNESS); + vec3 F = fresnelSchlick(max(dot(halfway, viewDir), 0.0), F0); + + vec3 kS = F; + vec3 kD = vec3(1.0) - kS; + kD *= 1.0 - MATERIAL_METALLIC; + + float NdotL = max(dot(normal, lightDir), 0.0); + + vec3 numerator = NDF * G * F; + float denominator = 4.0 * max(dot(normal, viewDir), 0.0) * NdotL + 0.0001; + vec3 specular = numerator / denominator; + + Lo += (kD * MATERIAL_ALBEDO / 3.14159 + specular) * radiance * NdotL; + } + + return Lo; +} + +void main() { + vec3 normal = normalize(fragNormal); + vec3 viewDir = normalize(pc.cameraPos - fragWorldPos); + + // Ambient lighting + vec3 ambient = AMBIENT_STRENGTH * MATERIAL_ALBEDO; + + // Direct lighting with PBR + vec3 lighting = calculateLighting(fragWorldPos, normal, viewDir); + + // Shadow calculation + float shadow = calculateShadow(fragWorldPos); + lighting *= (1.0 - shadow * 0.7); // Soften shadows + + // Combine lighting + vec3 finalColor = ambient + lighting; + + // Fog + float fogFactor = 1.0 - exp(-FOG_DENSITY * length(pc.cameraPos - fragWorldPos)); + finalColor = mix(finalColor, FOG_COLOR, fogFactor); + + // Simple tone mapping + finalColor = finalColor / (finalColor + vec3(1.0)); + + // Gamma correction + finalColor = pow(finalColor, vec3(1.0 / 2.2)); + + outColor = vec4(finalColor, 1.0); +} \ No newline at end of file diff --git a/shaders/pbr.frag.spv b/shaders/pbr.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..5458f9ab2daaa36394653a29027a4c673f8756aa GIT binary patch literal 11932 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 Ry-sbb`z`q1|E2OZ@E>x~oq_-W literal 0 HcmV?d00001 diff --git a/shaders/shadow.frag b/shaders/shadow.frag new file mode 100644 index 0000000..b7217da --- /dev/null +++ b/shaders/shadow.frag @@ -0,0 +1,6 @@ +#version 450 + +void main() { + // Empty fragment shader for shadow mapping + // Depth is automatically written +} \ No newline at end of file diff --git a/shaders/shadow.frag.spv b/shaders/shadow.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..757dd51bdd669a9f3efd078466cb07f5b583bc95 GIT binary patch literal 180 zcmZQ(Qf6mhU}WHC;9y{5fB-=TCZGrdP>c;syZZ$D=oOcw=$V)rfK>1?FoP9>)bKN~ zGOz$?5YA1^%mYaX0Odh^pus>EGte{!hC@KbAeG4Kgn=?3IYqE41~A(Us0t(oQU?Mc Rn|=UASb_9kpld9E7y!nA3km=L literal 0 HcmV?d00001 diff --git a/shaders/shadow.vert b/shaders/shadow.vert new file mode 100644 index 0000000..b60bd9d --- /dev/null +++ b/shaders/shadow.vert @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inTexCoord; + +layout(push_constant) uniform PushConstants { + mat4 model; + mat4 lightViewProj; +} pc; + +void main() { + gl_Position = pc.lightViewProj * pc.model * vec4(inPosition, 1.0); +} \ No newline at end of file diff --git a/shaders/shadow.vert.spv b/shaders/shadow.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..30ad42f3b03ffb48f4c6e9b665faf1a1d8f62942 GIT binary patch literal 1388 zcmZ9K-%b-z5XO&fx1cD9$UnuhSQM3nc%jCGD4Lp-3t~!YxHS#kz#-dRvb#j$6|aDg zb3TSF?Y)rde zFs4o25#h|3yUuRtI~v#o=YiyzWJB^sQrE90{Z|QrY3O&u4Rptp{G<41FUk(0tccF# z3N3MLVrgy*nD7>#3ePV6j>$FV^D(KCmB zW~F;)`Ee&r$S!jEO+~;tqjVU>(%c6|FR?v3E)Hz;tCyuG@~Jr|eR_x;xP91UJch&A zk4?U(*=RsHhp{i4{D{swX_^g<>8cLOYdE2XN}+^LD^|y~ncY8~%J1BcHjSN;3!j z5MPmo3r;= 1.0) return vec3(1.0); // Skybox + + vec3 worldPos = worldPosFromDepth(depth, texCoord); + vec3 normal = normalize(texture(normalBuffer, texCoord).rgb * 2.0 - 1.0); + + float occlusion = 0.0; + + for (int i = 0; i < NUM_SAMPLES; i++) { + // Generate sample position in hemisphere around normal + float angle = (float(i) / float(NUM_SAMPLES)) * 6.283185; + vec2 offset = vec2(cos(angle), sin(angle)) * SAMPLE_RADIUS; + + vec2 sampleTexCoord = texCoord + offset / textureSize(depthBuffer, 0); + float sampleDepth = texture(depthBuffer, sampleTexCoord).r; + + if (sampleDepth < depth - 0.01) { // Occluded + occlusion += 1.0; + } + } + + occlusion = 1.0 - (occlusion / float(NUM_SAMPLES)); + return vec3(occlusion); +} + +void main() { + vec3 sceneColor = texture(sceneColor, inTexCoord).rgb; + vec3 ao = ssao(inTexCoord); + + // Apply ambient occlusion + vec3 finalColor = sceneColor * (0.3 + 0.7 * ao); // Mix AO with direct lighting + + outColor = vec4(finalColor, 1.0); +} \ No newline at end of file diff --git a/shaders/ssgi.frag.spv b/shaders/ssgi.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..149e0a5fcb1178f3dc616e65a81534ede1c74f13 GIT binary patch literal 4644 zcmZve>yw;C6^A?7ofVQm!aacEE-@Dc1LOk4Xm*nT2@)zB1BrKcn4L)`%+9;a%qAdS z7BGs6AU7ox;Hsbq2;vpL_`z@f1y=cos89vP^UU;F-dgRd-=1@R=hEG$yWgFmQ0c+xBaXJU0y~`o7iV52K5jYO^iB z0DceN0&9rX$7ERj7TsGq#mr6h+1~L@jq7%vY;4(y z&zx1rY5BfgN#`JY^?UagMQ0k7m`XYy+3i+~O^bOI_VmAT7sJ(B&Dq1V)mlC0jv-Gr z>!&{8M$$UC$+_=EXXcQo54Cp*Q7Ma~|=9ia@|Am4r0sx$RYmCd0C zZzr4WUcKFI_U=Qk(4+Op+1g+)+tB+x$qTXvyWq+_*c0}ixkjVjp++T5M&4jLnBjB? zZnoN~&LFi9FR_ncw~Nk9wWWU~9bL3@;eK8vO(Ba~tu@ze79O~3X1S@^*M}47soFl) z8uWBHx!^}Co4$kx3 zPhE9K1?=Z<*xvUwOStewp7*4jr@b^!&b$c_^_^R8zQhe>?h)iZ*5#cT1IC7ZCAve~ zv(<0?u*-gXtlyzuyX;@Uz5+zPz0-CWZ{NbcfK|ATF?#1Z)h|XHL5}t@+~2o*m%D`g zv$AgNDE8S%{qp)(AdR)&7+qY6lrzTIF=Fh$oVGa^BF*u;W6g_@#>-!Yt#2)0OoV+| z-<%&`i{JelYyK5Pdj@hM)??j|VLPY(o!MTWxr?wjppSxOzx^C;F@4#MxdSnwA|9Hv-Sxy`Kvl zeyeUE&NE(>b#;EM4v3#@`p;cH4c|-W zt=C;*-(RwgKb>uRGJ(Dq>;c-|>zjc4if`J@=*Gy$-Q0&R@37DASw*fhOsc!#o&$fKj{T#Y;Ij4Qpj&eBf${oR<#m4z-J14Vzw7pX$g58wt6BG6Yy@x9yJ@7oRQ{X&HIT=EL;rE4ex>{0 zfNTP5yqVAJ?#!J)dQRGPU@ddiBX)NxKE0F#_k?Ums9;AEm9?Mx% ze-Fsn3+*{zFFZ$m5qA>39Cr%aIKS2U;@f;Lx;6C2H~j19^7jGP;MAU_7~L58c(dO}mv>n6G*ZqQ_V))sF6~fgmE7fQm|tUw~*NBMBPRD1m^W_`zi6Cb^o-#LP`0J1onU zRsIS0RJn8QTG#qZ{Hn6b=b3w2r#w|%r~B>q^y$;x_YMyoyC=)GWjnL&*=Jd`_GCjK z8Cbo)&&@B+Pxi|86S09(44HG%be&k>c zbnr|K5y!yV*tssr6$Ooh$#MHFCNSUprqboBzQu>*SJU3tPR_*>0y-Halex zyUxwxuC-!iwOnj%=6PqKi?hxteZHeC)^j=a6vsS^PMLRlMR^;&M(%sqH(FKhhtXTz zweDu$SIfrPd#${a&sL!MJSit9$maRNgj(F-n@)kRblaDkw=1%>I0JSoH^J(zDQxQ} ze~!J^T;Etr!Nsg^cdJadRXq8HPbh>nSR?(K9s&2=bG*wM z_}&ZfqgKbTU4h9J+dhDKMr;>iTx+1X!aiD=ecPP9*QMWl2W@lpym#r>j`MrT(EG;w zQ&iIN*xFOcc2>oXq_;!M^!?v|$dfpnH$Cj2h0M`x6NMCG@+$-fD#ZD)zPk|8;co znGXL4*f9q;(CvZm_P!>&^|lt)NP8(^ai4c**_}qriL>f`QB!Brb($J^kM(pJ_YaU7 zyIf~5`X9FcFW;+ezx|hleH`8YM%eoOUxcmSe?{2VZfd}mKbP#Y#Fyvy>6}lKow;a- zZ#TNwIWl%u_an}?GaQ`JgnyOh;Q+Sh#Et2D#DaeiUF_S$e;8fNxevaWeCPBiGM;Sn z^`Bx+*EnyF9!KPeU!E`{-iV5oh}b`-h0L?f)+F{wU#b?qh7vibw2E&<`U~ zi<{_Ta?MTOO~l+{Uftt-3ti5oRGS=K%oW%&x|n?oY$ahs>