Repository: lettier/3d-game-shaders-for-beginners Branch: master Commit: 29700852da7b Files: 121 Total size: 935.1 KB Directory structure: gitextract_oegtrd8a/ ├── .gitignore ├── README.md ├── demonstration/ │ ├── build-for-linux.sh │ ├── eggs/ │ │ └── mill-scene/ │ │ ├── banner.bam │ │ ├── mill-scene.bam │ │ ├── shutters.bam │ │ └── weather-vane.bam │ ├── panda3d-prc-file.prc │ ├── shaders/ │ │ ├── LICENSE │ │ ├── fragment/ │ │ │ ├── base-combine.frag │ │ │ ├── base.frag │ │ │ ├── bloom.frag │ │ │ ├── box-blur.frag │ │ │ ├── chromatic-aberration.frag │ │ │ ├── depth-of-field.frag │ │ │ ├── dilation.frag │ │ │ ├── discard.frag │ │ │ ├── film-grain.frag │ │ │ ├── foam-mask.frag │ │ │ ├── foam.frag │ │ │ ├── fog.frag │ │ │ ├── gamma-correction.frag │ │ │ ├── geometry-buffer-0.frag │ │ │ ├── geometry-buffer-1.frag │ │ │ ├── geometry-buffer-2.frag │ │ │ ├── kuwahara-filter.frag │ │ │ ├── lookup-table.frag │ │ │ ├── material-diffuse.frag │ │ │ ├── material-specular.frag │ │ │ ├── median-filter.frag │ │ │ ├── motion-blur.frag │ │ │ ├── normal.frag │ │ │ ├── outline.frag │ │ │ ├── pixelize.frag │ │ │ ├── position.frag │ │ │ ├── posterize.frag │ │ │ ├── reflection-color.frag │ │ │ ├── reflection.frag │ │ │ ├── refraction.frag │ │ │ ├── scene-combine.frag │ │ │ ├── screen-space-reflection.frag │ │ │ ├── screen-space-refraction.frag │ │ │ ├── sharpen.frag │ │ │ └── ssao.frag │ │ └── vertex/ │ │ ├── base.vert │ │ ├── basic.vert │ │ └── discard.vert │ ├── sounds/ │ │ ├── water.ogg │ │ └── wheel.ogg │ └── src/ │ ├── LICENSE │ └── main.cxx ├── docs/ │ ├── _build-docs.sh │ ├── _template.html5 │ ├── blinn-phong.html │ ├── bloom.html │ ├── blur.html │ ├── building-the-demo.html │ ├── cel-shading.html │ ├── chromatic-aberration.html │ ├── deferred-rendering.html │ ├── depth-of-field.html │ ├── dilation.html │ ├── film-grain.html │ ├── flow-mapping.html │ ├── foam.html │ ├── fog.html │ ├── fresnel-factor.html │ ├── gamma-correction.html │ ├── glsl.html │ ├── index.html │ ├── lighting.html │ ├── lookup-table.html │ ├── motion-blur.html │ ├── normal-mapping.html │ ├── outlining.html │ ├── pixelization.html │ ├── posterization.html │ ├── reference-frames.html │ ├── render-to-texture.html │ ├── rim-lighting.html │ ├── running-the-demo.html │ ├── screen-space-reflection.html │ ├── screen-space-refraction.html │ ├── setup.html │ ├── sharpen.html │ ├── ssao.html │ ├── style.css │ └── texturing.html └── sections/ ├── blinn-phong.md ├── bloom.md ├── blur.md ├── building-the-demo.md ├── cel-shading.md ├── chromatic-aberration.md ├── deferred-rendering.md ├── depth-of-field.md ├── dilation.md ├── film-grain.md ├── flow-mapping.md ├── foam.md ├── fog.md ├── fresnel-factor.md ├── gamma-correction.md ├── glsl.md ├── lighting.md ├── lookup-table.md ├── motion-blur.md ├── normal-mapping.md ├── outlining.md ├── pixelization.md ├── posterization.md ├── reference-frames.md ├── render-to-texture.md ├── rim-lighting.md ├── running-the-demo.md ├── screen-space-reflection.md ├── screen-space-refraction.md ├── setup.md ├── sharpen.md ├── ssao.md └── texturing.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ images/** blends/** *.kra~ *.png~ *main.o* *3d-game-shaders-for-beginners.o* *3d-game-shaders-for-beginners* !blends/* !blends/mill-scene/mill-scene-low-poly-backup.blend ================================================ FILE: README.md ================================================

3D Game Shaders For Beginners

# 3D Game Shaders For Beginners Interested in adding textures, lighting, shadows, normal maps, glowing objects, ambient occlusion, reflections, refractions, and more to your 3D game? Great! Below is a collection of shading techniques that will take your game visuals to new heights. I've explained each technique in such a way that you can take what you learn here and apply/port it to whatever stack you use—be it Godot, Unity, Unreal, or something else. For the glue in between the shaders, I've chosen the fabulous Panda3D game engine and the OpenGL Shading Language (GLSL). So if that is your stack, then you'll also get the benefit of learning how to use these shading techniques with Panda3D and OpenGL specifically. ## Table Of Contents - [Setup](sections/setup.md) - [Building The Demo](sections/building-the-demo.md) - [Running The Demo](sections/running-the-demo.md) - [Reference Frames](sections/reference-frames.md) - [GLSL](sections/glsl.md) - [Render To Texture](sections/render-to-texture.md) - [Texturing](sections/texturing.md) - [Lighting](sections/lighting.md) - [Blinn-Phong](sections/blinn-phong.md) - [Fresnel Factor](sections/fresnel-factor.md) - [Rim Lighting](sections/rim-lighting.md) - [Cel Shading](sections/cel-shading.md) - [Normal Mapping](sections/normal-mapping.md) - [Deferred Rendering](sections/deferred-rendering.md) - [Fog](sections/fog.md) - [Blur](sections/blur.md) - [Bloom](sections/bloom.md) - [SSAO](sections/ssao.md) - [Motion Blur](sections/motion-blur.md) - [Chromatic Aberration](sections/chromatic-aberration.md) - [Screen Space Reflection](sections/screen-space-reflection.md) - [Screen Space Refraction](sections/screen-space-refraction.md) - [Foam](sections/foam.md) - [Flow Mapping](sections/flow-mapping.md) - [Outlining](sections/outlining.md) - [Depth Of Field](sections/depth-of-field.md) - [Posterization](sections/posterization.md) - [Pixelization](sections/pixelization.md) - [Sharpen](sections/sharpen.md) - [Dilation](sections/dilation.md) - [Film Grain](sections/film-grain.md) - [Lookup Table (LUT)](sections/lookup-table.md) - [Gamma Correction](sections/gamma-correction.md) ## License The included license applies only to the software portion of 3D Game Shaders For Beginners— specifically the `.cxx`, `.vert`, and `.frag` source code files. No other portion of 3D Game Shaders For Beginners has been licensed for use. ## Attributions - [Kiwi Soda Font](https://fontenddev.com/fonts/kiwi-soda/) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) ================================================ FILE: demonstration/build-for-linux.sh ================================================ #!/usr/bin/env bash SCRIPT_PATH="$(cd "$(dirname "$0")"; pwd -P)" g++ \ -Wfatal-errors \ -c $SCRIPT_PATH/src/main.cxx \ -o $SCRIPT_PATH/3d-game-shaders-for-beginners.o \ -std=gnu++11 \ -O3 \ -I/usr/include/python3.9/ \ -I$P3D_INCLUDE_PATH g++ \ $SCRIPT_PATH/3d-game-shaders-for-beginners.o \ -o $SCRIPT_PATH/3d-game-shaders-for-beginners \ -L$P3D_LIB_PATH \ -lp3framework \ -lpanda \ -lpandafx \ -lpandaexpress \ -lpandaphysics \ -lp3dtoolconfig \ -lp3dtool \ -lpthread ================================================ FILE: demonstration/panda3d-prc-file.prc ================================================ load-display pandagl audio-library-name p3openal_audio win-origin -2 -2 win-size 1200 900 fullscreen #f framebuffer-hardware #t framebuffer-software #f framebuffer-srgb #f depth-bits 1 color-bits 1 1 1 alpha-bits 0 stencil-bits 0 multisamples 0 notify-level warning default-directnotify-level warning model-path $MAIN_DIR want-directtools #f want-tk #f want-pstats #f show-frame-rate-meter #f use-movietexture #t hardware-animated-vertices #f model-cache-dir $XDG_CACHE_HOME/panda3d model-cache-textures #f basic-shaders-only #f gl-coordinate-system default gl-version 3 2 textures-auto-power-2 1 textures-power-2 down sync-flip #f sync-video #f ================================================ FILE: demonstration/shaders/LICENSE ================================================ BSD 3-Clause License Copyright (c) 2019, David Lettier All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: demonstration/shaders/fragment/base-combine.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D baseTexture; uniform sampler2D refractionTexture; uniform sampler2D foamTexture; uniform sampler2D reflectionTexture; uniform sampler2D specularTexture; out vec4 fragColor; void main() { vec2 texSize = textureSize(baseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 base = texture(baseTexture, texCoord); vec4 refraction = texture(refractionTexture, texCoord); vec4 foam = texture(foamTexture, texCoord); vec4 reflection = texture(reflectionTexture, texCoord); vec4 specular = texture(specularTexture, texCoord); fragColor = base; fragColor.rgb = mix(fragColor.rgb, refraction.rgb, clamp(refraction.a, 0.0, 1.0)); fragColor.rgb = mix(fragColor.rgb, reflection.rgb, clamp(reflection.a, 0.0, 1.0)); fragColor.rgb = mix(fragColor.rgb, foam.rgb, clamp(foam.a, 0.0, 1.0)); fragColor.rgb += (specular.rgb * clamp(specular.a, 0.0, 1.0)); } ================================================ FILE: demonstration/shaders/fragment/base.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 #define NUMBER_OF_LIGHTS 4 #define MAX_SHININESS 127.75 #define MAX_FRESNEL_POWER 5.0 uniform float osg_FrameTime; uniform vec2 pi; uniform vec2 gamma; uniform mat4 trans_world_to_view; uniform mat4 trans_view_to_world; uniform sampler2D p3d_Texture0; uniform sampler2D p3d_Texture1; uniform sampler2D p3d_Texture2; uniform sampler2D flowTexture; uniform sampler2D ssaoBlurTexture; uniform struct { vec4 ambient ; vec4 diffuse ; vec4 emission ; vec3 specular ; float shininess ; } p3d_Material; uniform struct { vec4 ambient ; } p3d_LightModel; uniform struct p3d_LightSourceParameters { vec4 color ; vec4 ambient ; vec4 diffuse ; vec4 specular ; vec4 position ; vec3 spotDirection ; float spotExponent ; float spotCutoff ; float spotCosCutoff ; float constantAttenuation ; float linearAttenuation ; float quadraticAttenuation ; vec3 attenuation ; sampler2DShadow shadowMap ; mat4 shadowViewMatrix ; } p3d_LightSource[NUMBER_OF_LIGHTS]; uniform vec2 normalMapsEnabled; uniform vec2 fresnelEnabled; uniform vec2 rimLightEnabled; uniform vec2 blinnPhongEnabled; uniform vec2 celShadingEnabled; uniform vec2 flowMapsEnabled; uniform vec2 specularOnly; uniform vec2 isParticle; uniform vec2 isWater; uniform vec2 sunPosition; in vec4 vertexColor; in vec4 vertexInShadowSpaces[NUMBER_OF_LIGHTS]; in vec4 vertexPosition; in vec3 vertexNormal; in vec3 binormal; in vec3 tangent; in vec2 diffuseCoord; in vec2 normalCoord; out vec4 out0; out vec4 out1; void main() { vec3 shadowColor = pow(vec3(0.149, 0.220, 0.227), vec3(gamma.x)); int shadowSamples = 2; vec4 diffuseColor; if (isParticle.x == 1) { diffuseColor = texture(p3d_Texture0, diffuseCoord) * vertexColor; } else { diffuseColor = texture(p3d_Texture0, diffuseCoord); } diffuseColor.rgb = pow(diffuseColor.rgb, vec3(gamma.x)); vec3 materialSpecularColor = p3d_Material.specular; vec2 flow = texture(flowTexture, normalCoord).xy; flow = (flow - 0.5) * 2.0; flow.x = abs(flow.x) <= 0.02 ? 0.0 : flow.x; flow.y = abs(flow.y) <= 0.02 ? 0.0 : flow.y; vec4 normalTex = texture ( p3d_Texture1 , vec2 ( normalCoord.x + flowMapsEnabled.x * flow.x * osg_FrameTime , normalCoord.y + flowMapsEnabled.y * flow.y * osg_FrameTime ) ); vec3 normal; if (isParticle.x == 1) { normal = normalize((trans_world_to_view * vec4(0.0, 0.0, 1.0, 0.0)).xyz); } else if (normalMapsEnabled.x == 1) { vec3 normalRaw = normalize ( normalTex.rgb * 2.0 - 1.0 ); normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normalRaw ); } else { normal = normalize(vertexNormal); } vec4 specularMap = texture(p3d_Texture2, diffuseCoord); vec4 diffuse = vec4(0.0, 0.0, 0.0, diffuseColor.a); vec4 specular = vec4(0.0, 0.0, 0.0, diffuseColor.a); for (int i = 0; i < p3d_LightSource.length(); ++i) { vec3 lightDirection = p3d_LightSource[i].position.xyz - vertexPosition.xyz * p3d_LightSource[i].position.w; vec3 unitLightDirection = normalize(lightDirection); vec3 eyeDirection = normalize(-vertexPosition.xyz); vec3 reflectedDirection = normalize(-reflect(unitLightDirection, normal)); vec3 halfwayDirection = normalize(unitLightDirection + eyeDirection); float lightDistance = length(lightDirection); float attenuation = 1.0 / ( p3d_LightSource[i].constantAttenuation + p3d_LightSource[i].linearAttenuation * lightDistance + p3d_LightSource[i].quadraticAttenuation * (lightDistance * lightDistance) ); if (attenuation <= 0.0) { continue; } float diffuseIntensity = dot(normal, unitLightDirection); if (diffuseIntensity < 0.0) { continue; } diffuseIntensity = celShadingEnabled.x == 1 ? smoothstep(0.1, 0.2, diffuseIntensity) : diffuseIntensity; vec4 lightDiffuseColor = p3d_LightSource[i].diffuse; lightDiffuseColor.rgb = pow(lightDiffuseColor.rgb, vec3(gamma.x)); vec4 diffuseTemp = vec4 ( clamp ( diffuseColor.rgb * lightDiffuseColor.rgb * diffuseIntensity , 0.0 , 1.0 ) , diffuseColor.a ); float specularIntensity = ( blinnPhongEnabled.x == 1 ? clamp(dot(normal, halfwayDirection), 0.0, 1.0) : clamp(dot(eyeDirection, reflectedDirection), 0.0, 1.0) ); specularIntensity = ( celShadingEnabled.x == 1 ? smoothstep(0.9, 1.0, specularIntensity) : specularIntensity ); vec4 lightSpecularColor = p3d_LightSource[i].specular; lightSpecularColor.rgb = pow(lightSpecularColor.rgb, vec3(gamma.x)); vec4 materialSpecularColor = vec4(vec3(specularMap.r), diffuseColor.a); if (fresnelEnabled.x == 1) { float fresnelFactor = dot((blinnPhongEnabled.x == 1 ? halfwayDirection : normal), eyeDirection); fresnelFactor = max(fresnelFactor, 0.0); fresnelFactor = 1.0 - fresnelFactor; fresnelFactor = pow(fresnelFactor, specularMap.b * MAX_FRESNEL_POWER); materialSpecularColor.rgb = mix(materialSpecularColor.rgb, vec3(1.0), clamp(fresnelFactor, 0.0, 1.0)); } vec4 specularTemp = vec4(vec3(0.0), diffuseColor.a); specularTemp.rgb = lightSpecularColor.rgb * pow(specularIntensity, specularMap.g * MAX_SHININESS); specularTemp.rgb *= materialSpecularColor.rgb; specularTemp.rgb *= (1 - isParticle.x); specularTemp.rgb = clamp(specularTemp.rgb, 0.0, 1.0); float unitLightDirectionDelta = dot ( normalize(p3d_LightSource[i].spotDirection) , -unitLightDirection ); if (unitLightDirectionDelta < p3d_LightSource[i].spotCosCutoff) { continue; } float spotExponent = p3d_LightSource[i].spotExponent; diffuseTemp.rgb *= (spotExponent <= 0.0 ? 1.0 : pow(unitLightDirectionDelta, spotExponent)); vec2 shadowMapSize = textureSize(p3d_LightSource[i].shadowMap, 0); float inShadow = 0.0; float count = 0.0; for ( int si = -shadowSamples; si <= shadowSamples; ++si) { for (int sj = -shadowSamples; sj <= shadowSamples; ++sj) { inShadow += ( 1.0 - textureProj ( p3d_LightSource[i].shadowMap , vertexInShadowSpaces[i] + vec4(vec2(si, sj) / shadowMapSize, vec2(0.0)) ) ); count += 1.0; } } inShadow /= count; vec3 shadow = mix ( vec3(1.0) , shadowColor , inShadow ); diffuseTemp.rgb *= mix(shadow, vec3(1.0), isParticle.x); specularTemp.rgb *= mix(shadow, vec3(1.0), isParticle.x); diffuseTemp.rgb *= attenuation; specularTemp.rgb *= attenuation; diffuse.rgb += diffuseTemp.rgb; specular.rgb += specularTemp.rgb; } vec4 rimLight = vec4(vec3(0.0), diffuseColor.a); if (rimLightEnabled.x == 1) { rimLight.rgb = vec3 ( 1.0 - max ( 0.0 , dot(normalize(-vertexPosition.xyz), normalize(normal)) ) ); rimLight.rgb = ( celShadingEnabled.x == 1 ? smoothstep(0.3, 0.4, rimLight.rgb) : pow(rimLight.rgb, vec3(2.0)) * 1.2 ); rimLight.rgb *= diffuse.rgb; } vec2 ssaoBlurTexSize = textureSize(ssaoBlurTexture, 0).xy; vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize; vec3 ssao = texture(ssaoBlurTexture, ssaoBlurTexCoord).rgb; ssao = mix(shadowColor, vec3(1.0), clamp(ssao.r, 0.0, 1.0)); float sunPosition = sin(sunPosition.x * pi.y); float sunMixFactor = 1.0 - (sunPosition / 2.0 + 0.5); vec3 ambientCool = pow(vec3(0.302, 0.451, 0.471), vec3(gamma.x)) * max(0.5, sunMixFactor); vec3 ambientWarm = pow(vec3(0.765, 0.573, 0.400), vec3(gamma.x)) * max(0.5, sunMixFactor); vec3 skyLight = mix(ambientCool, ambientWarm, sunMixFactor); vec3 groundLight = mix(ambientWarm, ambientCool, sunMixFactor); vec3 worldNormal = normalize((trans_view_to_world * vec4(normal, 0.0)).xyz); vec3 ambientLight = mix ( groundLight , skyLight , 0.5 * (1.0 + dot(worldNormal, vec3(0, 0, 1))) ); vec3 ambient = ambientLight.rgb * diffuseColor.rgb * ssao; vec3 emission = p3d_Material.emission.rgb * max(0.1, pow(sunPosition, 0.4)); out0.a = diffuseColor.a; out0.rgb = ambient.rgb + diffuse.rgb + rimLight.rgb + emission.rgb; if (isWater.x == 1) { out0.a = 0.0; } out1.a = diffuseColor.a; out1.rgb = specular.rgb; if (isParticle.x == 1) { out1.rgb = vec3(0.0); } } ================================================ FILE: demonstration/shaders/fragment/bloom.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform vec2 enabled; out vec4 fragColor; void main() { int size = 3; float separation = 4.0; float threshold = 0.6; float amount = 0.6; if (enabled.x != 1 || size <= 0) { fragColor = vec4(0); return; } vec2 texSize = textureSize(colorTexture, 0).xy; vec4 result = vec4(0.0); vec4 color = vec4(0.0); float value = 0.0; float count = 0.0; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { color = texture ( colorTexture , (vec2(i, j) * separation + gl_FragCoord.xy) / texSize ); value = max(color.r, max(color.g, color.b)); if (value < threshold) { color = vec4(0.0); } result += color; count += 1.0; } } result /= count; fragColor = mix(vec4(0.0), result, amount); } ================================================ FILE: demonstration/shaders/fragment/box-blur.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform vec2 parameters; out vec4 fragColor; void main() { vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; fragColor = texture(colorTexture, texCoord); int size = int(parameters.x); if (size <= 0) { return; } float separation = parameters.y; separation = max(separation, 1); fragColor.rgb = vec3(0); float count = 0.0; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { fragColor.rgb += texture ( colorTexture , ( gl_FragCoord.xy + (vec2(i, j) * separation) ) / texSize ).rgb; count += 1.0; } } fragColor.rgb /= count; } ================================================ FILE: demonstration/shaders/fragment/chromatic-aberration.frag ================================================ /* (C) 2021 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform vec2 enabled; uniform vec2 mouseFocusPoint; out vec4 fragColor; void main() { float redOffset = 0.009; float greenOffset = 0.006; float blueOffset = -0.006; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec2 direction = texCoord - mouseFocusPoint; fragColor = texture(colorTexture, texCoord); if (enabled.x != 1) { return; } fragColor.r = texture(colorTexture, texCoord + (direction * vec2(redOffset ))).r; fragColor.g = texture(colorTexture, texCoord + (direction * vec2(greenOffset))).g; fragColor.b = texture(colorTexture, texCoord + (direction * vec2(blueOffset ))).b; } ================================================ FILE: demonstration/shaders/fragment/depth-of-field.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D positionTexture; uniform sampler2D noiseTexture; uniform sampler2D focusTexture; uniform sampler2D outOfFocusTexture; uniform vec2 mouseFocusPoint; uniform vec2 nearFar; uniform vec2 enabled; out vec4 fragColor; out vec4 fragColor1; void main() { float minDistance = 8.0; float maxDistance = 12.0; float far = nearFar.y; vec2 texSize = textureSize(focusTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 focusColor = texture(focusTexture, texCoord); fragColor = focusColor; if (enabled.x != 1) { return; } vec4 position = texture(positionTexture, texCoord); if (position.a <= 0) { fragColor1 = vec4(1.0); return; } vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord); vec4 focusPoint = texture(positionTexture, mouseFocusPoint); float blur = smoothstep ( minDistance , maxDistance , length(position - focusPoint) ); fragColor = mix(focusColor, outOfFocusColor, blur); fragColor1 = vec4(blur); } ================================================ FILE: demonstration/shaders/fragment/dilation.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform vec2 parameters; out vec4 fragColor; void main() { int size = int(parameters.x); float separation = parameters.y; float minThreshold = 0.2; float maxThreshold = 0.5; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 fragCoord = gl_FragCoord.xy; fragColor = texture(colorTexture, fragCoord / texSize); if (size <= 0) { return; } float mx = 0.0; vec4 cmx = fragColor; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { // For a rectangular shape. //if (false); // For a diamond shape; //if (!(abs(i) <= size - abs(j))) { continue; } // For a circular shape. if (!(distance(vec2(i, j), vec2(0, 0)) <= size)) { continue; } vec4 c = texture ( colorTexture , ( gl_FragCoord.xy + (vec2(i, j) * separation) ) / texSize ); float mxt = dot(c.rgb, vec3(0.3, 0.59, 0.11)); if (mxt > mx) { mx = mxt; cmx = c; } } } fragColor.rgb = mix ( fragColor.rgb , cmx.rgb , smoothstep(minThreshold, maxThreshold, mx) ); } ================================================ FILE: demonstration/shaders/fragment/discard.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 void main() { discard; } ================================================ FILE: demonstration/shaders/fragment/film-grain.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 pi; uniform float osg_FrameTime; uniform sampler2D colorTexture; uniform vec2 enabled; out vec4 fragColor; void main() { float amount = 0.01; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 color = texture(colorTexture, texCoord); if (enabled.x == 1) { float randomIntensity = fract ( 10000 * sin ( ( gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime ) * pi.y ) ); amount *= randomIntensity; color.rgb += amount; } fragColor = color; } ================================================ FILE: demonstration/shaders/fragment/foam-mask.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform float osg_FrameTime; uniform sampler2D foamPatternTexture; uniform sampler2D flowTexture; uniform vec2 flowMapsEnabled; in vec2 diffuseCoord; out vec4 fragColor; void main() { vec2 flow = texture(flowTexture, diffuseCoord).xy; flow = (flow - 0.5) * 2; flow.x = abs(flow.x) <= 0.02 ? 0 : flow.x; flow.y = abs(flow.y) <= 0.02 ? 0 : flow.y; vec4 foamPattern = texture ( foamPatternTexture , vec2 ( diffuseCoord.x + flowMapsEnabled.x * flow.x * osg_FrameTime , diffuseCoord.y + flowMapsEnabled.y * flow.y * osg_FrameTime ) ); fragColor = vec4(vec3(dot(foamPattern.rgb, vec3(1)) / 3), 1); } ================================================ FILE: demonstration/shaders/fragment/foam.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 pi; uniform vec2 gamma; uniform mat4 viewWorldMat; uniform sampler2D maskTexture; uniform sampler2D positionFromTexture; uniform sampler2D positionToTexture; uniform vec2 foamDepth; uniform vec2 sunPosition; out vec4 fragColor; void main() { vec4 foamColor = vec4(0.8, 0.85, 0.92, 0.8); vec2 texSize = textureSize(positionFromTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 mask = texture(maskTexture, texCoord); if (mask.r <= 0.0 || foamDepth.x <= 0.0) { fragColor = vec4(0.0); return; } foamColor.rgb = pow(foamColor.rgb, vec3(gamma.x)); foamColor.rgb *= max(0.4, -1 * sin(sunPosition.x * pi.y)); vec4 positionFrom = texture(positionFromTexture, texCoord); vec4 positionTo = texture(positionToTexture, texCoord); positionFrom = viewWorldMat * positionFrom; positionTo = viewWorldMat * positionTo; float depth = length(positionTo.xyz - positionFrom.xyz); float amount = clamp(depth / foamDepth.x, 0.0, 1.0); amount = 1.0 - amount; amount *= mask.r; // Ease in and out. amount = (amount * amount) / (2.0 * (amount * amount - amount) + 1.0); fragColor = vec4(foamColor.rgb, amount * foamColor.a); } ================================================ FILE: demonstration/shaders/fragment/fog.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 pi; uniform vec2 gamma; uniform vec4 backgroundColor0; uniform vec4 backgroundColor1; uniform sampler2D positionTexture0; uniform sampler2D positionTexture1; uniform sampler2D smokeMaskTexture; uniform vec3 origin; uniform vec2 nearFar; uniform vec2 sunPosition; uniform vec2 enabled; out vec4 fragColor; void main() { float fogMin = 0.00; float fogMax = 0.97; if (enabled.x != 1) { fragColor = vec4(0); return; } vec2 texSize = textureSize(positionTexture0, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 smokeMask = texture(smokeMaskTexture, texCoord); vec4 position0 = texture(positionTexture0, texCoord); position0.y -= origin.y; float near = nearFar.x; float far = nearFar.y; vec4 position1 = texture(positionTexture1, texCoord); position1.y -= origin.y; if (position1.a <= 0) { position1.y = far; } vec4 position = position1; if (position0.a <= 0 && smokeMask.r > 0) { position.y = mix(far, position1.y, smokeMask.r); } else if (position0.a > 0 && smokeMask.r > 0) { position.xyz = mix(position0.xyz, position1.xyz, smokeMask.r); } float random = fract ( 10000 * sin ( ( gl_FragCoord.x * 104729 + gl_FragCoord.y * 7639 ) * pi.y ) ); vec4 backgroundColor0 = backgroundColor0; vec4 backgroundColor1 = backgroundColor1; backgroundColor0.rgb = pow(backgroundColor0.rgb, vec3(gamma.x)); backgroundColor1.rgb = pow(backgroundColor1.rgb, vec3(gamma.x)); vec4 color = mix ( backgroundColor0 , backgroundColor1 , 1.0 - clamp(random * 0.1 + texCoord.y, 0.0, 1.0) ); float sunPosition = max(0.2, -1 * sin(sunPosition.x * pi.y)); color.rgb *= sunPosition; color.b = mix(color.b + 0.05, color.b, sunPosition); float intensity = clamp ( (position.y - near) / (far - near) , fogMin , fogMax ); fragColor = vec4(color.rgb, intensity); } ================================================ FILE: demonstration/shaders/fragment/gamma-correction.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 gamma; uniform sampler2D colorTexture; out vec4 fragColor; void main() { vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; fragColor = texture(colorTexture, texCoord); fragColor.rgb = pow(fragColor.rgb, vec3(gamma.y)); } ================================================ FILE: demonstration/shaders/fragment/geometry-buffer-0.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 uniform sampler2D p3d_Texture1; uniform vec2 normalMapsEnabled; in vec4 vertexPosition; in vec3 vertexNormal; in vec3 binormal; in vec3 tangent; in vec2 normalCoord; out vec4 positionOut; out vec4 normalOut; void main() { vec4 normalTex = texture ( p3d_Texture1 , normalCoord ); vec3 normal; if (normalMapsEnabled.x == 1) { normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); } else { normal = normalize(vertexNormal); } positionOut = vertexPosition; normalOut = vec4(normal, 1); } ================================================ FILE: demonstration/shaders/fragment/geometry-buffer-1.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 #define MAX_SHININESS 127.75 uniform float osg_FrameTime; uniform struct { vec3 specular ; float shininess ; } p3d_Material; uniform mat4 p3d_ProjectionMatrix; uniform sampler2D p3d_Texture1; uniform sampler2D p3d_Texture3; uniform sampler2D p3d_Texture4; uniform sampler2D flowTexture; uniform sampler2D foamPatternTexture; uniform vec2 normalMapsEnabled; uniform vec2 flowMapsEnabled; in vec4 vertexPosition; in vec4 vertexColor; in vec3 vertexNormal; in vec3 binormal; in vec3 tangent; in vec2 diffuseCoord; in vec2 normalCoord; out vec4 positionOut; out vec4 normalOut; out vec4 reflectionMaskOut; out vec4 refractionMaskOut; out vec4 foamMaskOut; void main() { vec2 flow = texture(flowTexture, normalCoord).xy; flow = (flow - 0.5) * 2.0; flow.x = abs(flow.x) <= 0.02 ? 0.0 : flow.x; flow.y = abs(flow.y) <= 0.02 ? 0.0 : flow.y; vec3 normal; if (normalMapsEnabled.x == 1) { vec4 normalTex = texture ( p3d_Texture1 , vec2 ( normalCoord.x + flowMapsEnabled.x * flow.x * osg_FrameTime , normalCoord.y + flowMapsEnabled.y * flow.y * osg_FrameTime ) ); normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); } else { normal = normalize(vertexNormal); } vec4 reflectionMask = texture(p3d_Texture3, diffuseCoord); vec4 refractionMask = texture(p3d_Texture4, diffuseCoord); vec2 foamPatternTextureSize = textureSize(foamPatternTexture, 0).xy; vec4 foamUvOffset = p3d_ProjectionMatrix * vec4(normalize(normal), 1.0); foamUvOffset.xyz /= foamUvOffset.w; foamUvOffset.xy = foamUvOffset.xy * 0.5 + 0.5; foamUvOffset.xy /= foamPatternTextureSize; foamUvOffset.xy *= 0.3; vec2 foamUv = diffuseCoord.xy + foamUvOffset.xy; foamUv = foamUv + flowMapsEnabled * flow * 0.5 * osg_FrameTime; foamUv *= 0.5; vec4 foamPattern = texture(foamPatternTexture, foamUv); positionOut = vertexPosition; normalOut = vec4(normal, 1.0); reflectionMaskOut = reflectionMask; refractionMaskOut = refractionMask; foamMaskOut = foamPattern; } ================================================ FILE: demonstration/shaders/fragment/geometry-buffer-2.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 uniform sampler2D p3d_Texture0; uniform sampler2D positionTexture; uniform vec2 isSmoke; in vec4 vertexPosition; in vec4 vertexColor; in vec2 diffuseCoord; out vec4 positionOut; out vec4 smokeMaskOut; void main() { positionOut = vertexPosition; smokeMaskOut = vec4(0.0); if (isSmoke.x == 1) { vec4 diffuseColor = texture(p3d_Texture0, diffuseCoord) * vertexColor; vec2 texSize = textureSize(positionTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 position = texture(positionTexture, texCoord); if (position.a <= 0.0) { positionOut = diffuseColor.a > 0.0 ? vertexPosition : vec4(0.0); } else { positionOut = mix(position, vertexPosition, diffuseColor.a); } smokeMaskOut = diffuseColor * vertexColor; smokeMaskOut.rgb = vec3(dot(smokeMaskOut.rgb, vec3(1.0 / 3.0))); } } ================================================ FILE: demonstration/shaders/fragment/kuwahara-filter.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 #define MAX_SIZE 5 #define MAX_KERNEL_SIZE ((MAX_SIZE * 2 + 1) * (MAX_SIZE * 2 + 1)) uniform sampler2D colorTexture; uniform vec2 parameters; out vec4 fragColor; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; int i = 0; int j = 0; int count = 0; vec3 valueRatios = vec3(0.3, 0.59, 0.11); float values[MAX_KERNEL_SIZE]; vec4 color = vec4(0.0); vec4 meanTemp = vec4(0.0); vec4 mean = vec4(0.0); float valueMean = 0.0; float variance = 0.0; float minVariance = -1.0; void findMean(int i0, int i1, int j0, int j1) { meanTemp = vec4(0); count = 0; for (i = i0; i <= i1; ++i) { for (j = j0; j <= j1; ++j) { color = texture ( colorTexture , (gl_FragCoord.xy + vec2(i, j)) / texSize ); meanTemp += color; values[count] = dot(color.rgb, valueRatios); count += 1; } } meanTemp.rgb /= count; valueMean = dot(meanTemp.rgb, valueRatios); for (i = 0; i < count; ++i) { variance += pow(values[i] - valueMean, 2); } variance /= count; if (variance < minVariance || minVariance <= -1) { mean = meanTemp; minVariance = variance; } } void main() { fragColor = texture(colorTexture, texCoord); int size = int(parameters.x); if (size <= 0) { return; } // Lower Left findMean(-size, 0, -size, 0); // Upper Right findMean(0, size, 0, size); // Upper Left findMean(-size, 0, 0, size); // Lower Right findMean(0, size, -size, 0); fragColor.rgb = mean.rgb; } ================================================ FILE: demonstration/shaders/fragment/lookup-table.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 uniform vec2 pi; uniform vec2 gamma; uniform sampler2D colorTexture; uniform sampler2D lookupTableTexture0; uniform sampler2D lookupTableTexture1; uniform vec2 sunPosition; uniform vec2 enabled; out vec4 fragColor; void main() { vec2 texSize = textureSize(colorTexture, 0).xy; vec4 color = texture(colorTexture, gl_FragCoord.xy / texSize); if (enabled.x != 1) { fragColor = color; return; } color.rgb = pow(color.rgb, vec3(gamma.y)); float u = floor(color.b * 15.0) / 15.0 * 240.0; u = (floor(color.r * 15.0) / 15.0 * 15.0) + u; u /= 255.0; float v = 1.0 - (floor(color.g * 15.0) / 15.0); vec3 left0 = texture(lookupTableTexture0, vec2(u, v)).rgb; vec3 left1 = texture(lookupTableTexture1, vec2(u, v)).rgb; u = ceil(color.b * 15.0) / 15.0 * 240.0; u = (ceil(color.r * 15.0) / 15.0 * 15.0) + u; u /= 255.0; v = 1.0 - (ceil(color.g * 15.0) / 15.0); vec3 right0 = texture(lookupTableTexture0, vec2(u, v)).rgb; vec3 right1 = texture(lookupTableTexture1, vec2(u, v)).rgb; float sunPosition = sin(sunPosition.x * pi.y); sunPosition = 0.5 * (sunPosition + 1); vec3 left = mix(left0, left1, sunPosition); vec3 right = mix(right0, right1, sunPosition); color.r = mix(left.r, right.r, fract(color.r * 15.0)); color.g = mix(left.g, right.g, fract(color.g * 15.0)); color.b = mix(left.b, right.b, fract(color.b * 15.0)); color.rgb = pow(color.rgb, vec3(gamma.x)); fragColor = color; } ================================================ FILE: demonstration/shaders/fragment/material-diffuse.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform struct { vec4 diffuse ; } p3d_Material; out vec4 fragColor; void main() { fragColor = p3d_Material.diffuse; } ================================================ FILE: demonstration/shaders/fragment/material-specular.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 #define MAX_SHININESS 127.75 uniform struct { vec3 specular ; float shininess ; } p3d_Material; out vec4 fragColor; void main() { fragColor = vec4 ( p3d_Material.specular , clamp(p3d_Material.shininess / MAX_SHININESS, 0.0, 1.0) ); } ================================================ FILE: demonstration/shaders/fragment/median-filter.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 #define MAX_SIZE 4 #define MAX_KERNEL_SIZE ((MAX_SIZE * 2 + 1) * (MAX_SIZE * 2 + 1)) #define MAX_BINS_SIZE 10 uniform sampler2D colorTexture; uniform vec2 parameters; out vec4 fragColor; void main() { vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; int size = int(parameters.x); if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; } if (size > MAX_SIZE) { size = MAX_SIZE; } int kernelSize = int(pow(size * 2 + 1, 2)); int binsSize = int(parameters.y); binsSize = clamp(binsSize, 1, MAX_BINS_SIZE); int i = 0; int j = 0; int count = 0; int binIndex = 0; vec4 colors[MAX_KERNEL_SIZE]; float bins[MAX_BINS_SIZE]; int binIndexes[colors.length()]; float total = 0; float limit = floor(float(kernelSize) / 2) + 1; float value = 0; vec3 valueRatios = vec3(0.3, 0.59, 0.11); for (i = -size; i <= size; ++i) { for (j = -size; j <= size; ++j) { colors[count] = texture ( colorTexture , ( gl_FragCoord.xy + vec2(i, j) ) / texSize ); count += 1; } } for (i = 0; i < binsSize; ++i) { bins[i] = 0; } for (i = 0; i < kernelSize; ++i) { value = dot(colors[i].rgb, valueRatios); binIndex = int(floor(value * binsSize)); binIndex = clamp(binIndex, 0, binsSize - 1); bins[binIndex] += 1; binIndexes[i] = binIndex; } binIndex = 0; for (i = 0; i < binsSize; ++i) { total += bins[i]; if (total >= limit) { binIndex = i; break; } } fragColor = colors[0]; for (i = 0; i < kernelSize; ++i) { if (binIndexes[i] == binIndex) { fragColor = colors[i]; break; } } } ================================================ FILE: demonstration/shaders/fragment/motion-blur.frag ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 uniform sampler2D positionTexture; uniform sampler2D colorTexture; uniform mat4 previousViewWorldMat; uniform mat4 worldViewMat; uniform mat4 lensProjection; uniform vec2 motionBlurEnabled; uniform vec2 parameters; out vec4 fragColor; void main() { int size = int(parameters.x); float separation = parameters.y; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; fragColor = texture(colorTexture, texCoord); vec4 position1 = texture(positionTexture, texCoord); if (size <= 0 || separation <= 0.0 || motionBlurEnabled.x != 1 || position1.a <= 0.0) { return; } vec4 position0 = worldViewMat * previousViewWorldMat * position1; position0 = lensProjection * position0; position0.xyz /= position0.w; position0.xy = position0.xy * 0.5 + 0.5; position1 = lensProjection * position1; position1.xyz /= position1.w; position1.xy = position1.xy * 0.5 + 0.5; vec2 direction = position1.xy - position0.xy; if (length(direction) <= 0.0) { return; } direction.xy *= separation; vec2 forward = texCoord; vec2 backward = texCoord; float count = 1.0; for (int i = 0; i < size; ++i) { forward += direction; backward -= direction; fragColor += texture ( colorTexture , forward ); fragColor += texture ( colorTexture , backward ); count += 2.0; } fragColor /= count; } ================================================ FILE: demonstration/shaders/fragment/normal.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform float osg_FrameTime; uniform sampler2D p3d_Texture1; uniform sampler2D flowTexture; uniform vec2 normalMapsEnabled; uniform vec2 flowMapsEnabled; in vec3 vertexNormal; in vec3 binormal; in vec3 tangent; in vec2 normalCoord; out vec4 fragColor; void main() { vec2 flow = texture(flowTexture, normalCoord).xy; flow = (flow - 0.5) * 2; flow.x = abs(flow.x) <= 0.02 ? 0 : flow.x; flow.y = abs(flow.y) <= 0.02 ? 0 : flow.y; vec4 normalTex = texture ( p3d_Texture1 , vec2 ( normalCoord.x + flowMapsEnabled.x * flow.x * osg_FrameTime , normalCoord.y + flowMapsEnabled.y * flow.y * osg_FrameTime ) ); vec3 normal; if (normalMapsEnabled.x == 1) { normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); } else { normal = normalize(vertexNormal); } // To convert Panda3D z-up to OpenGL y-up. // fragColor = vec4(normal.x, normal.z, -normal.y, 1); fragColor = vec4(normal, 1); } ================================================ FILE: demonstration/shaders/fragment/outline.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 gamma; uniform sampler2D positionTexture; uniform sampler2D colorTexture; uniform sampler2D noiseTexture; uniform sampler2D depthOfFieldTexture; uniform sampler2D fogTexture; uniform vec2 nearFar; uniform vec2 enabled; out vec4 fragColor; void main() { float minSeparation = 1.0; float maxSeparation = 1.0; float minDistance = 1.5; float maxDistance = 2.0; float noiseScale = 1.0; int size = 1; vec3 colorModifier = vec3(0.522, 0.431, 0.349); colorModifier = pow(colorModifier, vec3(gamma.x)); float near = nearFar.x; float far = nearFar.y; vec2 fragCoord = gl_FragCoord.xy; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = fragCoord / texSize; vec4 color = texture(colorTexture, texCoord); float depthOfField = texture(depthOfFieldTexture, texCoord).r; float fog = texture(fogTexture, texCoord).a; if (enabled.x != 1) { fragColor = color; return; } fragColor = vec4(0.0); vec2 noise = texture(noiseTexture, fragCoord / textureSize(noiseTexture, 0).xy).rb; noise = noise * 2.0 - 1.0; noise *= noiseScale; texCoord = (fragCoord - noise) / texSize; vec4 position = texture(positionTexture, texCoord); vec4 positionTemp = position; if (position.a <= 0.0) { position.y = far; } float depth = clamp ( 1.0 - ( (far - position.y) / (far - near) ) , 0.0 , 1.0 ); float separation = mix(maxSeparation, minSeparation, depth); float count = 1.0; float mx = 0.0; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { texCoord = (vec2(i, j) * separation + (fragCoord + noise)) / texSize; positionTemp = texture ( positionTexture , texCoord ); if (positionTemp.y <= 0.0) { positionTemp.y = far; } mx = max(mx, abs(position.y - positionTemp.y)); depthOfField = max ( texture ( depthOfFieldTexture , texCoord ).r , depthOfField ); fog += texture ( fogTexture , texCoord ).a; count += 1.0; } } depthOfField = 1.0 - clamp(depthOfField, 0.0, 1.0); fog = 1.0 - clamp(fog / count, 0.0, 1.0); float diff = smoothstep(minDistance, maxDistance, mx) * depthOfField * fog; texCoord = fragCoord / texSize; vec3 lineColor = texture(colorTexture, texCoord).rgb; lineColor *= colorModifier; fragColor.rgb = mix(color.rgb, lineColor, clamp(diff, 0.0, 1.0)); fragColor.a = 1.0; } ================================================ FILE: demonstration/shaders/fragment/pixelize.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform sampler2D positionTexture; uniform vec2 parameters; uniform vec2 enabled; out vec4 fragColor; void main() { // Must be odd. int pixelSize = int(parameters.x); vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; // Avoid the background. vec4 position = texture(positionTexture, texCoord); if (enabled.x != 1 || position.a <= 0.0) { fragColor = texture(colorTexture, texCoord); return; } float x = int(gl_FragCoord.x) % pixelSize; float y = int(gl_FragCoord.y) % pixelSize; x = floor(pixelSize / 2.0) - x; y = floor(pixelSize / 2.0) - y; x = gl_FragCoord.x + x; y = gl_FragCoord.y + y; vec2 uv = vec2(x, y) / texSize; fragColor = texture(colorTexture, uv); } ================================================ FILE: demonstration/shaders/fragment/position.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 in vec4 vertexPosition; out vec4 fragColor; void main() { // To convert Panda3D z-up to OpenGL y-up. /* fragColor = vertexPosition.xzyw; */ fragColor = vertexPosition; } ================================================ FILE: demonstration/shaders/fragment/posterize.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 gamma; uniform sampler2D colorTexture; uniform sampler2D positionTexture; uniform vec2 enabled; out vec4 fragColor; void main() { float levels = 6.0; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; // Avoid the background. vec4 position = texture(positionTexture, texCoord); if (position.a <= 0) { fragColor = vec4(0); return; } fragColor = texture(colorTexture, texCoord); if (enabled.x != 1) { return; } fragColor.rgb = pow(fragColor.rgb, vec3(gamma.y)); float greyscale = max(fragColor.r, max(fragColor.g, fragColor.b)); float lower = floor(greyscale * levels) / levels; float lowerDiff = abs(greyscale - lower); float upper = ceil(greyscale * levels) / levels; float upperDiff = abs(upper - greyscale); float level = lowerDiff <= upperDiff ? lower : upper; float adjustment = level / greyscale; fragColor.rgb = fragColor.rgb * adjustment; fragColor.rgb = pow(fragColor.rgb, vec3(gamma.x)); } ================================================ FILE: demonstration/shaders/fragment/reflection-color.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D uvTexture; uniform sampler2D colorTexture; out vec4 fragColor; void main() { int size = 6; float separation = 2.0; vec2 texSize = textureSize(uvTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 uv = texture(uvTexture, texCoord); // Removes holes in the UV map. if (uv.b <= 0.0) { uv = vec4(0.0); float count = 0.0; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { uv += texture ( uvTexture , ( (vec2(i, j) * separation) + gl_FragCoord.xy ) / texSize ); count += 1.0; } } uv.xyz /= count; } if (uv.b <= 0.0) { fragColor = vec4(0.0); return;} vec4 color = texture(colorTexture, uv.xy); float alpha = clamp(uv.b, 0.0, 1.0); fragColor = vec4(mix(vec3(0.0), color.rgb, alpha), alpha); } ================================================ FILE: demonstration/shaders/fragment/reflection.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform sampler2D colorBlurTexture; uniform sampler2D maskTexture; out vec4 fragColor; void main() { vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 mask = texture(maskTexture, texCoord); vec4 color = texture(colorTexture, texCoord); vec4 colorBlur = texture(colorBlurTexture, texCoord); float amount = clamp(mask.r, 0.0, 1.0); if (amount <= 0.0) { fragColor = vec4(0.0); return; } float roughness = clamp(mask.g, 0.0, 1.0); fragColor = mix(color, colorBlur, roughness) * amount; } ================================================ FILE: demonstration/shaders/fragment/refraction.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 pi; uniform vec2 gamma; uniform sampler2D uvTexture; uniform sampler2D maskTexture; uniform sampler2D positionFromTexture; uniform sampler2D positionToTexture; uniform sampler2D backgroundColorTexture; uniform vec2 sunPosition; out vec4 fragColor; void main() { vec4 tintColor = vec4(0.392, 0.537, 0.561, 0.8); float depthMax = 2.0; vec2 texSize = textureSize(backgroundColorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 backgroundColor = texture(backgroundColorTexture, texCoord); vec4 mask = texture(maskTexture, texCoord); if (mask.r <= 0) { fragColor = backgroundColor; return; } vec4 uv = texture(uvTexture, texCoord); if (uv.b <= 0) { fragColor = backgroundColor; return; } tintColor.rgb = pow(tintColor.rgb, vec3(gamma.x)); tintColor.rgb *= max(0.2, -1 * sin(sunPosition.x * pi.y)); vec4 positionFrom = texture(positionFromTexture, texCoord); vec4 positionTo = texture(positionToTexture, uv.xy); backgroundColor = texture(backgroundColorTexture, uv.xy); float depth = length(positionTo.xyz - positionFrom.xyz); float mixture = clamp(depth / depthMax, 0.0, 1.0); vec3 shallowColor = backgroundColor.rgb; vec3 deepColor = mix(shallowColor, tintColor.rgb, tintColor.a); vec3 foregroundColor = mix(shallowColor, deepColor, mixture); fragColor = mix(vec4(0.0), vec4(foregroundColor, 1.0), uv.b); } ================================================ FILE: demonstration/shaders/fragment/scene-combine.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform vec2 pi; uniform vec2 gamma; uniform sampler2D baseTexture; uniform sampler2D bloomTexture; uniform sampler2D fogTexture; uniform vec4 backgroundColor0; uniform vec4 backgroundColor1; uniform vec2 sunPosition; out vec4 fragColor; void main() { vec2 texSize = textureSize(baseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 backgroundColor0 = backgroundColor0; vec4 backgroundColor1 = backgroundColor1; backgroundColor0.rgb = pow(backgroundColor0.rgb, vec3(gamma.x)); backgroundColor1.rgb = pow(backgroundColor1.rgb, vec3(gamma.x)); float random = fract ( 10000 * sin ( ( gl_FragCoord.x * 104729 + gl_FragCoord.y * 7639 ) * pi.y ) ); float sunPosition = sin(sunPosition.x * pi.y); sunPosition = max(0.2, -1 * sunPosition); vec4 backgroundColor = mix ( backgroundColor0 , backgroundColor1 , 1.0 - clamp(random * 0.1 + texCoord.y, 0.0, 1.0) ); backgroundColor.rgb *= sunPosition; backgroundColor.b = mix(backgroundColor.b + 0.05, backgroundColor.b, sunPosition); vec4 baseColor = texture(baseTexture, texCoord); vec4 bloomColor = texture(bloomTexture, texCoord); vec4 fogColor = texture(fogTexture, texCoord); fragColor = baseColor; fragColor = fragColor + bloomColor; fragColor = mix(fragColor, fogColor, min(fogColor.a, 1)); fragColor = vec4 ( mix ( backgroundColor.rgb , fragColor.rgb , min(baseColor.a + fogColor.a, 1) ) , 1 ); } ================================================ FILE: demonstration/shaders/fragment/screen-space-reflection.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform mat4 lensProjection; uniform sampler2D positionTexture; uniform sampler2D normalTexture; uniform sampler2D maskTexture; uniform vec2 enabled; out vec4 fragColor; void main() { float maxDistance = 8; float resolution = 0.3; int steps = 5; float thickness = 0.5; vec2 texSize = textureSize(positionTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 uv = vec4(0.0); vec4 positionFrom = texture(positionTexture, texCoord); vec4 mask = texture(maskTexture, texCoord); if ( positionFrom.w <= 0.0 || enabled.x != 1.0 || mask.r <= 0.0 ) { fragColor = uv; return; } vec3 unitPositionFrom = normalize(positionFrom.xyz); vec3 normal = normalize(texture(normalTexture, texCoord).xyz); vec3 pivot = normalize(reflect(unitPositionFrom, normal)); vec4 positionTo = positionFrom; vec4 startView = vec4(positionFrom.xyz + (pivot * 0.0), 1.0); vec4 endView = vec4(positionFrom.xyz + (pivot * maxDistance), 1.0); vec4 startFrag = startView; startFrag = lensProjection * startFrag; startFrag.xyz /= startFrag.w; startFrag.xy = startFrag.xy * 0.5 + 0.5; startFrag.xy *= texSize; vec4 endFrag = endView; endFrag = lensProjection * endFrag; endFrag.xyz /= endFrag.w; endFrag.xy = endFrag.xy * 0.5 + 0.5; endFrag.xy *= texSize; vec2 frag = startFrag.xy; uv.xy = frag / texSize; float deltaX = endFrag.x - startFrag.x; float deltaY = endFrag.y - startFrag.y; float useX = abs(deltaX) >= abs(deltaY) ? 1.0 : 0.0; float delta = mix(abs(deltaY), abs(deltaX), useX) * clamp(resolution, 0.0, 1.0); vec2 increment = vec2(deltaX, deltaY) / max(delta, 0.001); float search0 = 0; float search1 = 0; int hit0 = 0; int hit1 = 0; float viewDistance = startView.y; float depth = thickness; float i = 0; for (i = 0; i < int(delta); ++i) { frag += increment; uv.xy = frag / texSize; positionTo = texture(positionTexture, uv.xy); search1 = mix ( (frag.y - startFrag.y) / deltaY , (frag.x - startFrag.x) / deltaX , useX ); search1 = clamp(search1, 0.0, 1.0); viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); depth = viewDistance - positionTo.y; if (depth > 0 && depth < thickness) { hit0 = 1; break; } else { search0 = search1; } } search1 = search0 + ((search1 - search0) / 2.0); steps *= hit0; for (i = 0; i < steps; ++i) { frag = mix(startFrag.xy, endFrag.xy, search1); uv.xy = frag / texSize; positionTo = texture(positionTexture, uv.xy); viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); depth = viewDistance - positionTo.y; if (depth > 0 && depth < thickness) { hit1 = 1; search1 = search0 + ((search1 - search0) / 2); } else { float temp = search1; search1 = search1 + ((search1 - search0) / 2); search0 = temp; } } float visibility = hit1 * positionTo.w * ( 1 - max ( dot(-unitPositionFrom, pivot) , 0 ) ) * ( 1 - clamp ( depth / thickness , 0 , 1 ) ) * ( 1 - clamp ( length(positionTo - positionFrom) / maxDistance , 0 , 1 ) ) * (uv.x < 0 || uv.x > 1 ? 0 : 1) * (uv.y < 0 || uv.y > 1 ? 0 : 1); visibility = clamp(visibility, 0, 1); uv.ba = vec2(visibility); fragColor = uv; } ================================================ FILE: demonstration/shaders/fragment/screen-space-refraction.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform mat4 lensProjection; uniform sampler2D positionFromTexture; uniform sampler2D positionToTexture; uniform sampler2D normalFromTexture; uniform vec2 rior; uniform vec2 enabled; out vec4 fragColor; void main() { float maxDistance = 5; float resolution = 0.3; int steps = 5; float thickness = 0.5; vec2 texSize = textureSize(positionFromTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 uv = vec4(texCoord.xy, 1, 1); vec4 positionFrom = texture(positionFromTexture, texCoord); if (positionFrom.w <= 0 || enabled.x != 1) { fragColor = uv; return; } vec3 unitPositionFrom = normalize(positionFrom.xyz); vec3 normalFrom = normalize(texture(normalFromTexture, texCoord).xyz); vec3 pivot = normalize(refract(unitPositionFrom, normalFrom, rior.x)); vec4 positionTo = positionFrom; vec4 startView = vec4(positionFrom.xyz + (pivot * 0), 1); vec4 endView = vec4(positionFrom.xyz + (pivot * maxDistance), 1); vec4 startFrag = startView; startFrag = lensProjection * startFrag; startFrag.xyz /= startFrag.w; startFrag.xy = startFrag.xy * 0.5 + 0.5; startFrag.xy *= texSize; vec4 endFrag = endView; endFrag = lensProjection * endFrag; endFrag.xyz /= endFrag.w; endFrag.xy = endFrag.xy * 0.5 + 0.5; endFrag.xy *= texSize; vec2 frag = startFrag.xy; uv.xy = frag / texSize; float deltaX = endFrag.x - startFrag.x; float deltaY = endFrag.y - startFrag.y; float useX = abs(deltaX) >= abs(deltaY) ? 1 : 0; float delta = mix(abs(deltaY), abs(deltaX), useX) * clamp(resolution, 0, 1); vec2 increment = vec2(deltaX, deltaY) / max(delta, 0.001); float search0 = 0; float search1 = 0; int hit0 = 0; int hit1 = 0; float viewDistance = startView.y; float depth = thickness; float i = 0; for (i = 0; i < int(delta); ++i) { frag += increment; uv.xy = frag / texSize; positionTo = texture(positionToTexture, uv.xy); search1 = mix ( (frag.y - startFrag.y) / deltaY , (frag.x - startFrag.x) / deltaX , useX ); search1 = clamp(search1, 0, 1); viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); depth = viewDistance - positionTo.y; if (depth > 0 && depth < thickness) { hit0 = 1; break; } else { search0 = search1; } } search1 = search0 + ((search1 - search0) / 2); steps *= hit0; for (i = 0; i < steps; ++i) { frag = mix(startFrag.xy, endFrag.xy, search1); uv.xy = frag / texSize; positionTo = texture(positionToTexture, uv.xy); viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); depth = viewDistance - positionTo.y; if (depth > 0 && depth < thickness) { hit1 = 1; search1 = search0 + ((search1 - search0) / 2); } else { float temp = search1; search1 = search1 + ((search1 - search0) / 2); search0 = temp; } } float visibility = hit1 * positionTo.w * ( 1 - max ( dot(-unitPositionFrom, pivot) , 0 ) ) * (uv.x < 0 || uv.x > 1 ? 0 : 1) * (uv.y < 0 || uv.y > 1 ? 0 : 1); visibility = clamp(visibility, 0, 1); fragColor = vec4(mix(texCoord.xy, uv.xy, visibility), 1, 1); } ================================================ FILE: demonstration/shaders/fragment/sharpen.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform sampler2D colorTexture; uniform vec2 enabled; out vec4 fragColor; void main() { float amount = 0.3; vec2 texSize = textureSize(colorTexture, 0).xy; vec2 fragCoord = gl_FragCoord.xy; vec2 texCoord = fragCoord / texSize; if (enabled.x != 1) { fragColor = texture(colorTexture, texCoord); return; } float neighbor = amount * -1.0; float center = amount * 4.0 + 1.0; vec3 color = texture(colorTexture, (fragCoord + vec2( 0, 1)) / texSize).rgb * neighbor + texture(colorTexture, (fragCoord + vec2(-1, 0)) / texSize).rgb * neighbor + texture(colorTexture, (fragCoord + vec2( 0, 0)) / texSize).rgb * center + texture(colorTexture, (fragCoord + vec2( 1, 0)) / texSize).rgb * neighbor + texture(colorTexture, (fragCoord + vec2( 0, -1)) / texSize).rgb * neighbor ; fragColor = vec4(color, texture(colorTexture, texCoord).a); } ================================================ FILE: demonstration/shaders/fragment/ssao.frag ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 #define NUM_SAMPLES 8 #define NUM_NOISE 4 uniform mat4 lensProjection; uniform vec3 samples[NUM_SAMPLES]; uniform vec3 noise[NUM_NOISE]; uniform sampler2D positionTexture; uniform sampler2D normalTexture; uniform vec2 enabled; out vec4 fragColor; void main() { float radius = 0.6; float bias = 0.005; float magnitude = 1.1; float contrast = 1.1; fragColor = vec4(1); if (enabled.x != 1) { return; } vec2 texSize = textureSize(positionTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 position = texture(positionTexture, texCoord); if (position.a <= 0) { return; } vec3 normal = normalize(texture(normalTexture, texCoord).xyz); int noiseS = int(sqrt(NUM_NOISE)); int noiseX = int(gl_FragCoord.x - 0.5) % noiseS; int noiseY = int(gl_FragCoord.y - 0.5) % noiseS; vec3 random = noise[noiseX + (noiseY * noiseS)]; vec3 tangent = normalize(random - normal * dot(random, normal)); vec3 binormal = cross(normal, tangent); mat3 tbn = mat3(tangent, binormal, normal); float occlusion = NUM_SAMPLES; for (int i = 0; i < NUM_SAMPLES; ++i) { vec3 samplePosition = tbn * samples[i]; samplePosition = position.xyz + samplePosition * radius; vec4 offsetUV = vec4(samplePosition, 1.0); offsetUV = lensProjection * offsetUV; offsetUV.xyz /= offsetUV.w; offsetUV.xy = offsetUV.xy * 0.5 + 0.5; // Config.prc // gl-coordinate-system default // textures-auto-power-2 1 // textures-power-2 down vec4 offsetPosition = texture(positionTexture, offsetUV.xy); float occluded = 0; if (samplePosition.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; } float intensity = smoothstep ( 0 , 1 , radius / abs(position.y - offsetPosition.y) ); occluded *= intensity; occlusion -= occluded; } occlusion /= NUM_SAMPLES; occlusion = pow(occlusion, magnitude); occlusion = contrast * (occlusion - 0.5) + 0.5; fragColor = vec4(vec3(occlusion), position.a); } ================================================ FILE: demonstration/shaders/vertex/base.vert ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 #define NUMBER_OF_LIGHTS 4 uniform mat4 p3d_ModelViewMatrix; uniform mat4 p3d_ProjectionMatrix; uniform mat3 p3d_NormalMatrix; uniform struct p3d_LightSourceParameters { vec4 color ; vec4 ambient ; vec4 diffuse ; vec4 specular ; vec4 position ; vec3 spotDirection ; float spotExponent ; float spotCutoff ; float spotCosCutoff ; float constantAttenuation ; float linearAttenuation ; float quadraticAttenuation ; vec3 attenuation ; sampler2DShadow shadowMap ; mat4 shadowViewMatrix ; } p3d_LightSource[NUMBER_OF_LIGHTS]; in vec4 p3d_Vertex; in vec3 p3d_Normal; in vec4 p3d_Color; in vec2 p3d_MultiTexCoord0; in vec2 p3d_MultiTexCoord1; in vec3 p3d_Binormal; in vec3 p3d_Tangent; out vec4 vertexPosition; out vec4 vertexColor; out vec3 vertexNormal; out vec3 binormal; out vec3 tangent; out vec2 normalCoord; out vec2 diffuseCoord; out vec4 vertexInShadowSpaces[NUMBER_OF_LIGHTS]; void main() { vertexColor = p3d_Color; vertexPosition = p3d_ModelViewMatrix * p3d_Vertex; vertexNormal = normalize(p3d_NormalMatrix * p3d_Normal); binormal = normalize(p3d_NormalMatrix * p3d_Binormal); tangent = normalize(p3d_NormalMatrix * p3d_Tangent); normalCoord = p3d_MultiTexCoord0; diffuseCoord = p3d_MultiTexCoord1; for (int i = 0; i < p3d_LightSource.length(); ++i) { vertexInShadowSpaces[i] = p3d_LightSource[i].shadowViewMatrix * vertexPosition; } gl_Position = p3d_ProjectionMatrix * vertexPosition; } ================================================ FILE: demonstration/shaders/vertex/basic.vert ================================================ /* (C) 2019 David Lettier lettier.com */ #version 150 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } ================================================ FILE: demonstration/shaders/vertex/discard.vert ================================================ /* (C) 2020 David Lettier lettier.com */ #version 150 void main() { gl_Position = vec4(vec3(2.0), 1.0); } ================================================ FILE: demonstration/src/LICENSE ================================================ BSD 3-Clause License Copyright (c) 2019, David Lettier All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: demonstration/src/main.cxx ================================================ /* (C) 2019 David Lettier lettier.com */ #include #include #include #include #include #include #include #include "pandaFramework.h" // Panda3D 1.10.9 #include "renderBuffer.h" #include "load_prc_file.h" #include "pStatClient.h" #include "pandaSystem.h" #include "mouseButton.h" #include "mouseWatcher.h" #include "buttonRegistry.h" #include "orthographicLens.h" #include "ambientLight.h" #include "directionalLight.h" #include "pointLight.h" #include "spotlight.h" #include "shader.h" #include "nodePathCollection.h" #include "auto_bind.h" #include "animControlCollection.h" #include "cardMaker.h" #include "fontPool.h" #include "texturePool.h" #include "particleSystemManager.h" #include "physicsManager.h" #include "spriteParticleRenderer.h" #include "pointParticleFactory.h" #include "pointEmitter.h" #include "physicalNode.h" #include "forceNode.h" #include "linearNoiseForce.h" #include "linearVectorForce.h" #include "linearJitterForce.h" #include "linearCylinderVortexForce.h" #include "linearEulerIntegrator.h" #include "audioManager.h" #include "audioSound.h" // STRUCTURES struct FramebufferTexture { PT(GraphicsOutput) buffer ; PT(DisplayRegion) bufferRegion ; PT(Camera) camera ; NodePath cameraNP ; NodePath shaderNP ; }; struct FramebufferTextureArguments { PT(WindowFramework) window ; PT(GraphicsOutput) graphicsOutput ; PT(GraphicsEngine) graphicsEngine ; GraphicsOutput::RenderTexturePlane bitplane ; LVecBase4 rgbaBits ; LColor clearColor ; int aux_rgba ; bool setFloatColor ; bool setSrgbColor ; bool setRgbColor ; bool useScene ; std::string name ; }; // END STRUCTURES // FUNCTIONS void generateLights ( NodePath render , bool showLights ); void generateWindowLight ( std::string name , NodePath render , LVecBase3 position , bool show ); float animateLights ( NodePath render , AnimControlCollection shuttersAnimationCollection , float delta , float speed , bool& closedShutters , bool middayDown , bool midnightDown ); PT(Shader) loadShader ( std::string vert , std::string frag ); FramebufferTexture generateFramebufferTexture ( FramebufferTextureArguments framebufferTextureArguments ); PTA_LVecBase3f generateSsaoSamples ( int numberOfSamples ); PTA_LVecBase3f generateSsaoNoise ( int numberOfNoise ); void showBuffer ( NodePath render2d , NodePath statusNP , std::tuple bufferTexture , bool alpha ); void hideBuffer ( NodePath render2d ); int microsecondsSinceEpoch ( ); bool isButtonDown ( PT(MouseWatcher) mouseWatcher , std::string character ); PT(MouseWatcher) getMouseWatcher ( WindowFramework* window ); void setSoundOff ( PT(AudioSound) sound ); void setSoundOn ( PT(AudioSound) sound ); void setSoundState ( PT(AudioSound) sound , bool on ); void updateAudoManager ( NodePath sceneRootNP , NodePath cameraNP ); LVecBase3f calculateCameraPosition ( double radius , double phi , double theta , LVecBase3 cameraLookAt ); LVecBase3f calculateCameraLookAt ( double upDownAdjust , double leftRightAdjust , double phi , double theta , LVecBase3 lookAt ); NodePath setUpParticles ( NodePath render , PT(Texture) smokeTexture ); void squashGeometry ( NodePath environmentNP ); double microsecondToSecond ( int m ); double toRadians ( double d ); LVecBase2f makeEnabledVec ( int t ); LVecBase2f toggleEnabledVec ( LVecBase2f vec ); void setTextureToNearestAndClamp ( PT(Texture) texture ); LColor mixColor ( LColor a , LColor b , float amount ); // END FUNCTIONS // GLOBALS const float TO_RAD = M_PI / 180.0; const LVecBase2f PI_SHADER_INPUT = LVecBase2f(M_PI, TO_RAD); const float GAMMA = 2.2; const float GAMMA_REC = 1.0 / GAMMA; const LVecBase2f GAMMA_SHADER_INPUT = LVecBase2f(GAMMA, GAMMA_REC); const int BACKGROUND_RENDER_SORT_ORDER = 10; const int UNSORTED_RENDER_SORT_ORDER = 50; const int SSAO_SAMPLES = 8; const int SSAO_NOISE = 4; const int SHADOW_SIZE = 2048; LVecBase4f sunlightColor0 = LVecBase4f ( 0.612 , 0.365 , 0.306 , 1 ); LVecBase4f sunlightColor1 = LVecBase4f ( 0.765 , 0.573 , 0.400 , 1 ); LVecBase4f moonlightColor0 = LVecBase4f ( 0.247 , 0.384 , 0.404 , 1 ); LVecBase4f moonlightColor1 = LVecBase4f ( 0.392 , 0.537 , 0.571 , 1 ); LVecBase4f windowLightColor = LVecBase4f ( 0.765 , 0.573 , 0.400 , 1 ); std::uniform_real_distribution randomFloats ( 0.0 , 1.0 ); std::default_random_engine generator; PT(AsyncTaskManager) taskManager = AsyncTaskManager::get_global_ptr(); PT(AudioManager) audioManager = AudioManager::create_AudioManager(); ParticleSystemManager particleSystemManager = ParticleSystemManager(); PhysicsManager physicsManager = PhysicsManager(); // END GLOBALS // MAIN int main ( int argc , char *argv[] ) { LColor backgroundColor [] = { LColor ( 0.392 , 0.537 , 0.561 , 1 ) , LColor ( 0.953 , 0.733 , 0.525 , 1 ) }; double cameraRotatePhiInitial = 67.5095; double cameraRotateThetaInitial = 231.721; double cameraRotateRadiusInitial = 1100.83; LVecBase3 cameraLookAtInitial = LVecBase3(1.00839, 1.20764, 5.85055); float cameraFov = 1.0; int cameraNear = 150; int cameraFar = 2000; LVecBase2f cameraNearFar = LVecBase2f(cameraNear, cameraFar); double cameraRotateRadius = cameraRotateRadiusInitial; double cameraRotatePhi = cameraRotatePhiInitial; double cameraRotateTheta = cameraRotateThetaInitial; LVecBase3 cameraLookAt = cameraLookAtInitial; float fogNearInitial = 2.0; float fogFarInitial = 9.0; float fogNear = fogNearInitial; float fogFar = fogFarInitial; float fogAdjust = 0.1; LVecBase2f foamDepthInitial = LVecBase2f(1.5, 1.5); float foamDepthAdjust = 0.1; LVecBase2f foamDepth = foamDepthInitial; LVecBase2f mouseThen = LVecBase2f(0.0, 0.0); LVecBase2f mouseNow = mouseThen; bool mouseWheelDown = false; bool mouseWheelUp = false; LVecBase2f riorInitial = LVecBase2f(1.05, 1.05); float riorAdjust = 0.005; LVecBase2f rior = riorInitial; LVecBase2f mouseFocusPointInitial = LVecBase2f(0.509167, 0.598); LVecBase2f mouseFocusPoint = mouseFocusPointInitial; float sunlightP = 260; bool animateSunlight = true; bool soundEnabled = true; bool soundStarted = false; float startSoundAt = 0.5; bool closedShutters = true; float statusAlpha = 1.0; LColor statusColor = LColor(0.9, 0.9, 1.0, statusAlpha); LColor statusShadowColor = LColor(0.1, 0.1, 0.3, statusAlpha); float statusFadeRate = 2.0; std::string statusText = "Ready"; LVecBase2f ssaoEnabled = makeEnabledVec(1); LVecBase2f blinnPhongEnabled = makeEnabledVec(1); LVecBase2f fresnelEnabled = makeEnabledVec(1); LVecBase2f rimLightEnabled = makeEnabledVec(1); LVecBase2f refractionEnabled = makeEnabledVec(1); LVecBase2f reflectionEnabled = makeEnabledVec(1); LVecBase2f fogEnabled = makeEnabledVec(1); LVecBase2f outlineEnabled = makeEnabledVec(1); LVecBase2f celShadingEnabled = makeEnabledVec(1); LVecBase2f normalMapsEnabled = makeEnabledVec(1); LVecBase2f bloomEnabled = makeEnabledVec(1); LVecBase2f sharpenEnabled = makeEnabledVec(1); LVecBase2f depthOfFieldEnabled = makeEnabledVec(1); LVecBase2f filmGrainEnabled = makeEnabledVec(1); LVecBase2f flowMapsEnabled = makeEnabledVec(1); LVecBase2f lookupTableEnabled = makeEnabledVec(1); LVecBase2f painterlyEnabled = makeEnabledVec(0); LVecBase2f motionBlurEnabled = makeEnabledVec(0); LVecBase2f posterizeEnabled = makeEnabledVec(0); LVecBase2f pixelizeEnabled = makeEnabledVec(0); LVecBase2f chromaticAberrationEnabled = makeEnabledVec(1); LVecBase4 rgba8 = ( 8, 8, 8, 8); LVecBase4 rgba16 = (16, 16, 16, 16); LVecBase4 rgba32 = (32, 32, 32, 32); load_prc_file("panda3d-prc-file.prc"); PT(TextFont) font = FontPool::load_font("fonts/font.ttf"); std::vector sounds = { audioManager->get_sound("sounds/wheel.ogg", true) , audioManager->get_sound("sounds/water.ogg", true) }; PT(Texture) blankTexture = TexturePool::load_texture("images/blank.png"); PT(Texture) foamPatternTexture = TexturePool::load_texture("images/foam-pattern.png"); PT(Texture) stillFlowTexture = TexturePool::load_texture("images/still-flow.png"); PT(Texture) upFlowTexture = TexturePool::load_texture("images/up-flow.png"); PT(Texture) colorLookupTableTextureN = TexturePool::load_texture("images/lookup-table-neutral.png"); PT(Texture) colorLookupTableTexture0 = TexturePool::load_texture("images/lookup-table-0.png"); PT(Texture) colorLookupTableTexture1 = TexturePool::load_texture("images/lookup-table-1.png"); PT(Texture) smokeTexture = TexturePool::load_texture("images/smoke.png"); PT(Texture) colorNoiseTexture = TexturePool::load_texture("images/color-noise.png"); setTextureToNearestAndClamp(colorLookupTableTextureN); setTextureToNearestAndClamp(colorLookupTableTexture0); setTextureToNearestAndClamp(colorLookupTableTexture1); PandaFramework framework; framework.open_framework(argc, argv); framework.set_window_title("3D Game Shaders For Beginners By David Lettier"); PT(WindowFramework) window = framework.open_window(); PT(GraphicsWindow) graphicsWindow = window->get_graphics_window(); PT(GraphicsOutput) graphicsOutput = window->get_graphics_output(); PT(GraphicsStateGuardian) graphicsStateGuardian = graphicsOutput->get_gsg(); PT(GraphicsEngine) graphicsEngine = graphicsStateGuardian->get_engine(); window->enable_keyboard(); PT(DisplayRegion) displayRegion3d = window->get_display_region_3d(); displayRegion3d->set_clear_color_active(true); displayRegion3d->set_clear_depth_active(true); displayRegion3d->set_clear_stencil_active(true); displayRegion3d->set_clear_color(backgroundColor[1]); displayRegion3d->set_clear_depth(1.0f); displayRegion3d->set_clear_stencil(0); NodePath render = window->get_render(); NodePath render2d = window->get_render_2d(); PT(TextNode) status = new TextNode("status"); status->set_font(font); status->set_text(statusText); status->set_text_color(statusColor); status->set_shadow(0.0, 0.06); status->set_shadow_color(statusShadowColor); NodePath statusNP = render2d.attach_new_node(status); statusNP.set_scale(0.05); statusNP.set_pos(-0.96, 0, -0.95); PT(MouseWatcher) mouseWatcher = getMouseWatcher(window); PT(Camera) mainCamera = window->get_camera(0); PT(Lens) mainLens = mainCamera->get_lens(); mainLens->set_fov(cameraFov); mainLens->set_near_far(cameraNear, cameraFar); NodePath cameraNP = window->get_camera_group(); cameraNP.set_pos ( calculateCameraPosition ( cameraRotateRadius , cameraRotatePhi , cameraRotateTheta , cameraLookAt ) ); cameraNP.look_at(cameraLookAt); PT(PandaNode) sceneRootPN = new PandaNode("sceneRoot"); NodePath sceneRootNP = NodePath(sceneRootPN); sceneRootNP.reparent_to(render); NodePath environmentNP = window ->load_model ( framework.get_models() , "eggs/mill-scene/mill-scene.bam" ); environmentNP.reparent_to(sceneRootNP); NodePath shuttersNP = window ->load_model ( framework.get_models() , "eggs/mill-scene/shutters.bam" ); shuttersNP.reparent_to(sceneRootNP); NodePath weatherVaneNP = window ->load_model ( framework.get_models() , "eggs/mill-scene/weather-vane.bam" ); weatherVaneNP.reparent_to(sceneRootNP); NodePath bannerNP = window ->load_model ( framework.get_models() , "eggs/mill-scene/banner.bam" ); bannerNP.reparent_to(sceneRootNP); NodePath wheelNP = environmentNP.find("**/wheel-lp"); NodePath waterNP = environmentNP.find("**/water-lp"); squashGeometry(environmentNP); NodePath smokeNP = setUpParticles(render, smokeTexture); waterNP.set_transparency(TransparencyAttrib::M_dual); waterNP.set_bin("fixed", 0); AnimControlCollection shuttersAnimationCollection; AnimControlCollection weatherVaneAnimationCollection; AnimControlCollection bannerAnimationCollection; auto_bind ( shuttersNP.node() , shuttersAnimationCollection , PartGroup::HMF_ok_wrong_root_name | PartGroup::HMF_ok_part_extra | PartGroup::HMF_ok_anim_extra ); auto_bind ( weatherVaneNP.node() , weatherVaneAnimationCollection , PartGroup::HMF_ok_wrong_root_name | PartGroup::HMF_ok_part_extra | PartGroup::HMF_ok_anim_extra ); auto_bind ( bannerNP.node() , bannerAnimationCollection , PartGroup::HMF_ok_wrong_root_name | PartGroup::HMF_ok_part_extra | PartGroup::HMF_ok_anim_extra ); generateLights(render, false); PT(Shader) discardShader = loadShader("discard", "discard"); PT(Shader) baseShader = loadShader("base", "base"); PT(Shader) geometryBufferShader0 = loadShader("base", "geometry-buffer-0"); PT(Shader) geometryBufferShader1 = loadShader("base", "geometry-buffer-1"); PT(Shader) geometryBufferShader2 = loadShader("base", "geometry-buffer-2"); PT(Shader) foamShader = loadShader("basic", "foam"); PT(Shader) fogShader = loadShader("basic", "fog"); PT(Shader) boxBlurShader = loadShader("basic", "box-blur"); PT(Shader) motionBlurShader = loadShader("basic", "motion-blur"); PT(Shader) kuwaharaFilterShader = loadShader("basic", "kuwahara-filter"); PT(Shader) dilationShader = loadShader("basic", "dilation"); PT(Shader) sharpenShader = loadShader("basic", "sharpen"); PT(Shader) outlineShader = loadShader("basic", "outline"); PT(Shader) bloomShader = loadShader("basic", "bloom"); PT(Shader) ssaoShader = loadShader("basic", "ssao"); PT(Shader) screenSpaceRefractionShader = loadShader("basic", "screen-space-refraction"); PT(Shader) screenSpaceReflectionShader = loadShader("basic", "screen-space-reflection"); PT(Shader) refractionShader = loadShader("basic", "refraction"); PT(Shader) reflectionColorShader = loadShader("basic", "reflection-color"); PT(Shader) reflectionShader = loadShader("basic", "reflection"); PT(Shader) baseCombineShader = loadShader("basic", "base-combine"); PT(Shader) sceneCombineShader = loadShader("basic", "scene-combine"); PT(Shader) depthOfFieldShader = loadShader("basic", "depth-of-field"); PT(Shader) posterizeShader = loadShader("basic", "posterize"); PT(Shader) pixelizeShader = loadShader("basic", "pixelize"); PT(Shader) filmGrainShader = loadShader("basic", "film-grain"); PT(Shader) lookupTableShader = loadShader("basic", "lookup-table"); PT(Shader) gammaCorrectionShader = loadShader("basic", "gamma-correction"); PT(Shader) chromaticAberrationShader = loadShader("basic", "chromatic-aberration"); NodePath mainCameraNP = NodePath("mainCamera"); mainCameraNP.set_shader(discardShader); mainCamera->set_initial_state(mainCameraNP.get_state()); NodePath isWaterNP = NodePath("isWater"); isWaterNP.set_shader_input("isWater", LVecBase2f(1.0, 1.0)); isWaterNP.set_shader_input("flowTexture", upFlowTexture); isWaterNP.set_shader_input("foamPatternTexture", foamPatternTexture); NodePath isSmokeNP = NodePath("isSmoke"); isSmokeNP.set_shader_input("isSmoke", LVecBase2f(1.0, 1.0)); isSmokeNP.set_shader_input("isParticle", LVecBase2f(1.0, 1.0)); LMatrix4 currentViewWorldMat = cameraNP.get_transform(render)->get_mat(); LMatrix4 previousViewWorldMat = previousViewWorldMat; FramebufferTextureArguments framebufferTextureArguments; framebufferTextureArguments.window = window; framebufferTextureArguments.graphicsOutput = graphicsOutput; framebufferTextureArguments.graphicsEngine = graphicsEngine; framebufferTextureArguments.bitplane = GraphicsOutput::RTP_color; framebufferTextureArguments.rgbaBits = rgba32; framebufferTextureArguments.clearColor = LColor(0, 0, 0, 0); framebufferTextureArguments.aux_rgba = 1; framebufferTextureArguments.setFloatColor = true; framebufferTextureArguments.setSrgbColor = false; framebufferTextureArguments.setRgbColor = true; framebufferTextureArguments.useScene = true; framebufferTextureArguments.name = "geometry0"; FramebufferTexture geometryFramebufferTexture0 = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) geometryBuffer0 = geometryFramebufferTexture0.buffer; PT(Camera) geometryCamera0 = geometryFramebufferTexture0.camera; NodePath geometryNP0 = geometryFramebufferTexture0.shaderNP; geometryBuffer0->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_0 ); geometryBuffer0->set_clear_active(3, true); geometryBuffer0->set_clear_value( 3, framebufferTextureArguments.clearColor); geometryNP0.set_shader(geometryBufferShader0); geometryNP0.set_shader_input("normalMapsEnabled", normalMapsEnabled); geometryCamera0->set_initial_state(geometryNP0.get_state()); geometryCamera0->set_camera_mask(BitMask32::bit(1)); PT(Texture) positionTexture0 = geometryBuffer0->get_texture(0); PT(Texture) normalTexture0 = geometryBuffer0->get_texture(1); PT(Lens) geometryCameraLens0 = geometryCamera0->get_lens(); waterNP.hide(BitMask32::bit(1)); smokeNP.hide(BitMask32::bit(1)); framebufferTextureArguments.aux_rgba = 4; framebufferTextureArguments.name = "geometry1"; FramebufferTexture geometryFramebufferTexture1 = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) geometryBuffer1 = geometryFramebufferTexture1.buffer; PT(Camera) geometryCamera1 = geometryFramebufferTexture1.camera; NodePath geometryNP1 = geometryFramebufferTexture1.shaderNP; geometryBuffer1->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_0 ); geometryBuffer1->set_clear_active(3, true); geometryBuffer1->set_clear_value( 3, framebufferTextureArguments.clearColor); geometryBuffer1->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_1 ); geometryBuffer1->set_clear_active(4, true); geometryBuffer1->set_clear_value( 4, framebufferTextureArguments.clearColor); geometryBuffer1->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_2 ); geometryBuffer1->set_clear_active(5, true); geometryBuffer1->set_clear_value( 5, framebufferTextureArguments.clearColor); geometryBuffer1->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_3 ); geometryBuffer1->set_clear_active(6, true); geometryBuffer1->set_clear_value( 6, framebufferTextureArguments.clearColor); geometryNP1.set_shader(geometryBufferShader1); geometryNP1.set_shader_input("normalMapsEnabled", normalMapsEnabled); geometryNP1.set_shader_input("flowTexture", stillFlowTexture); geometryNP1.set_shader_input("foamPatternTexture", blankTexture); geometryNP1.set_shader_input("flowMapsEnabled", flowMapsEnabled); geometryCamera1->set_initial_state(geometryNP1.get_state()); geometryCamera1->set_tag_state_key("geometryBuffer1"); geometryCamera1->set_tag_state("isWater", isWaterNP.get_state()); geometryCamera1->set_camera_mask(BitMask32::bit(2)); PT(Texture) positionTexture1 = geometryBuffer1->get_texture(0); PT(Texture) normalTexture1 = geometryBuffer1->get_texture(1); PT(Texture) reflectionMaskTexture = geometryBuffer1->get_texture(2); PT(Texture) refractionMaskTexture = geometryBuffer1->get_texture(3); PT(Texture) foamMaskTexture = geometryBuffer1->get_texture(4); PT(Lens) geometryCameraLens1 = geometryCamera1->get_lens(); waterNP.set_tag("geometryBuffer1", "isWater"); smokeNP.hide(BitMask32::bit(2)); framebufferTextureArguments.aux_rgba = 1; framebufferTextureArguments.name = "geometry2"; FramebufferTexture geometryFramebufferTexture2 = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) geometryBuffer2 = geometryFramebufferTexture2.buffer; PT(Camera) geometryCamera2 = geometryFramebufferTexture2.camera; NodePath geometryNP2 = geometryFramebufferTexture2.shaderNP; geometryBuffer2->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_0 ); geometryBuffer2->set_clear_active(3, true); geometryBuffer2->set_clear_value( 3, framebufferTextureArguments.clearColor); geometryBuffer2->set_sort(geometryBuffer1->get_sort() + 1); geometryNP2.set_shader(geometryBufferShader2); geometryNP2.set_shader_input("isSmoke", LVecBase2f(0, 0)); geometryNP2.set_shader_input("positionTexture", positionTexture1); geometryCamera2->set_initial_state(geometryNP2.get_state()); geometryCamera2->set_tag_state_key("geometryBuffer2"); geometryCamera2->set_tag_state("isSmoke", isSmokeNP.get_state()); smokeNP.set_tag("geometryBuffer2", "isSmoke"); PT(Texture) positionTexture2 = geometryBuffer2->get_texture(0); PT(Texture) smokeMaskTexture = geometryBuffer2->get_texture(1); PT(Lens) geometryCameraLens2 = geometryCamera2->get_lens(); framebufferTextureArguments.rgbaBits = rgba8; framebufferTextureArguments.aux_rgba = 0; framebufferTextureArguments.clearColor = LColor(0, 0, 0, 0); framebufferTextureArguments.setFloatColor = false; framebufferTextureArguments.useScene = false; framebufferTextureArguments.name = "fog"; FramebufferTexture fogFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) fogBuffer = fogFramebufferTexture.buffer; PT(Camera) fogCamera = fogFramebufferTexture.camera; NodePath fogNP = fogFramebufferTexture.shaderNP; fogBuffer->set_sort(geometryBuffer2->get_sort() + 1); fogNP.set_shader(fogShader); fogNP.set_shader_input("pi", PI_SHADER_INPUT); fogNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); fogNP.set_shader_input("backgroundColor0", backgroundColor[0]); fogNP.set_shader_input("backgroundColor1", backgroundColor[1]); fogNP.set_shader_input("positionTexture0", positionTexture1); fogNP.set_shader_input("positionTexture1", positionTexture2); fogNP.set_shader_input("smokeMaskTexture", smokeMaskTexture); fogNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); fogNP.set_shader_input("origin", cameraNP.get_relative_point(render, environmentNP.get_pos())); fogNP.set_shader_input("nearFar", LVecBase2f(fogNear, fogFar)); fogNP.set_shader_input("enabled", fogEnabled); fogCamera->set_initial_state(fogNP.get_state()); PT(Texture) fogTexture = fogBuffer->get_texture(); framebufferTextureArguments.clearColor = LColor(1, 1, 1, 0); framebufferTextureArguments.name = "ssao"; FramebufferTexture ssaoFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) ssaoBuffer = ssaoFramebufferTexture.buffer; PT(Camera) ssaoCamera = ssaoFramebufferTexture.camera; NodePath ssaoNP = ssaoFramebufferTexture.shaderNP; ssaoBuffer->set_sort(geometryBuffer0->get_sort() + 1); ssaoNP.set_shader(ssaoShader); ssaoNP.set_shader_input("positionTexture", positionTexture0); ssaoNP.set_shader_input("normalTexture", normalTexture0); ssaoNP.set_shader_input("samples", generateSsaoSamples(SSAO_SAMPLES)); ssaoNP.set_shader_input("noise", generateSsaoNoise(SSAO_NOISE)); ssaoNP.set_shader_input("lensProjection", geometryCameraLens0->get_projection_mat()); ssaoNP.set_shader_input("enabled", ssaoEnabled); ssaoCamera->set_initial_state(ssaoNP.get_state()); framebufferTextureArguments.name = "ssaoBlur"; FramebufferTexture ssaoBlurFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) ssaoBlurBuffer = ssaoBlurFramebufferTexture.buffer; NodePath ssaoBlurNP = ssaoBlurFramebufferTexture.shaderNP; ssaoBlurBuffer->set_sort(ssaoBuffer->get_sort() + 1); ssaoBlurNP.set_shader(kuwaharaFilterShader); ssaoBlurNP.set_shader_input("colorTexture", ssaoBuffer->get_texture()); ssaoBlurNP.set_shader_input("parameters", LVecBase2f(1, 0)); ssaoBlurFramebufferTexture.camera->set_initial_state(ssaoBlurNP.get_state()); PT(Texture) ssaoBlurTexture = ssaoBlurBuffer->get_texture(); framebufferTextureArguments.rgbaBits = rgba16; framebufferTextureArguments.clearColor = LColor(0, 0, 0, 0); framebufferTextureArguments.name = "refractionUv"; FramebufferTexture refractionUvFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) refractionUvBuffer = refractionUvFramebufferTexture.buffer; PT(Camera) refractionUvCamera = refractionUvFramebufferTexture.camera; NodePath refractionUvNP = refractionUvFramebufferTexture.shaderNP; refractionUvBuffer->set_sort(geometryBuffer1->get_sort() + 1); refractionUvNP.set_shader(screenSpaceRefractionShader); refractionUvNP.set_shader_input("positionFromTexture", positionTexture1); refractionUvNP.set_shader_input("positionToTexture", positionTexture0); refractionUvNP.set_shader_input("normalFromTexture", normalTexture1); refractionUvNP.set_shader_input("lensProjection", geometryCameraLens0->get_projection_mat()); refractionUvNP.set_shader_input("enabled", refractionEnabled); refractionUvNP.set_shader_input("rior", rior); refractionUvCamera->set_initial_state(refractionUvNP.get_state()); PT(Texture) refractionUvTexture = refractionUvBuffer->get_texture(); framebufferTextureArguments.name = "reflectionUv"; FramebufferTexture reflectionUvFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) reflectionUvBuffer = reflectionUvFramebufferTexture.buffer; PT(Camera) reflectionUvCamera = reflectionUvFramebufferTexture.camera; NodePath reflectionUvNP = reflectionUvFramebufferTexture.shaderNP; reflectionUvBuffer->set_sort(geometryBuffer1->get_sort() + 1); reflectionUvNP.set_shader(screenSpaceReflectionShader); reflectionUvNP.set_shader_input("positionTexture", positionTexture1); reflectionUvNP.set_shader_input("normalTexture", normalTexture1); reflectionUvNP.set_shader_input("maskTexture", reflectionMaskTexture); reflectionUvNP.set_shader_input("lensProjection", geometryCameraLens0->get_projection_mat()); reflectionUvNP.set_shader_input("enabled", reflectionEnabled); reflectionUvCamera->set_initial_state(reflectionUvNP.get_state()); PT(Texture) reflectionUvTexture = reflectionUvBuffer->get_texture(); framebufferTextureArguments.rgbaBits = rgba8; framebufferTextureArguments.aux_rgba = 1; framebufferTextureArguments.useScene = true; framebufferTextureArguments.name = "base"; FramebufferTexture baseFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) baseBuffer = baseFramebufferTexture.buffer; PT(Camera) baseCamera = baseFramebufferTexture.camera; NodePath baseNP = baseFramebufferTexture.shaderNP; baseBuffer->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_0 ); baseBuffer->set_clear_active(3, true); baseBuffer->set_clear_value( 3, framebufferTextureArguments.clearColor); baseBuffer->set_sort ( std::max ( ssaoBlurBuffer->get_sort() + 1 , UNSORTED_RENDER_SORT_ORDER + 1 ) ); baseNP.set_shader(baseShader); baseNP.set_shader_input("pi", PI_SHADER_INPUT); baseNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); baseNP.set_shader_input("ssaoBlurTexture", ssaoBlurTexture); baseNP.set_shader_input("flowTexture", stillFlowTexture); baseNP.set_shader_input("normalMapsEnabled", normalMapsEnabled); baseNP.set_shader_input("blinnPhongEnabled", blinnPhongEnabled); baseNP.set_shader_input("fresnelEnabled", fresnelEnabled); baseNP.set_shader_input("rimLightEnabled", rimLightEnabled); baseNP.set_shader_input("celShadingEnabled", celShadingEnabled); baseNP.set_shader_input("flowMapsEnabled", flowMapsEnabled); baseNP.set_shader_input("specularOnly", LVecBase2f(0, 0)); baseNP.set_shader_input("isParticle", LVecBase2f(0, 0)); baseNP.set_shader_input("isWater", LVecBase2f(0, 0)); baseNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); baseCamera->set_initial_state(baseNP.get_state()); baseCamera->set_tag_state_key("baseBuffer"); baseCamera->set_tag_state("isParticle", isSmokeNP.get_state()); baseCamera->set_tag_state("isWater", isWaterNP.get_state()); baseCamera->set_camera_mask(BitMask32::bit(6)); smokeNP.set_tag("baseBuffer", "isParticle"); waterNP.set_tag("baseBuffer", "isWater"); PT(Texture) baseTexture = baseBuffer->get_texture(0); PT(Texture) specularTexture = baseBuffer->get_texture(1); framebufferTextureArguments.aux_rgba = 0; framebufferTextureArguments.useScene = false; framebufferTextureArguments.name = "refraction"; FramebufferTexture refractionFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) refractionBuffer = refractionFramebufferTexture.buffer; PT(Camera) refractionCamera = refractionFramebufferTexture.camera; NodePath refractionNP = refractionFramebufferTexture.shaderNP; refractionBuffer->set_sort(baseBuffer->get_sort() + 1); refractionNP.set_shader(refractionShader); refractionNP.set_shader_input("pi", PI_SHADER_INPUT); refractionNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); refractionNP.set_shader_input("uvTexture", refractionUvTexture); refractionNP.set_shader_input("maskTexture", refractionMaskTexture); refractionNP.set_shader_input("positionFromTexture", positionTexture1); refractionNP.set_shader_input("positionToTexture", positionTexture0); refractionNP.set_shader_input("backgroundColorTexture", baseTexture); refractionNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); refractionCamera->set_initial_state(refractionNP.get_state()); PT(Texture) refractionTexture = refractionBuffer->get_texture(); framebufferTextureArguments.name = "foam"; FramebufferTexture foamFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) foamBuffer = foamFramebufferTexture.buffer; PT(Camera) foamCamera = foamFramebufferTexture.camera; NodePath foamNP = foamFramebufferTexture.shaderNP; foamBuffer->set_sort(geometryBuffer1->get_sort() + 1); foamNP.set_shader(foamShader); foamNP.set_shader_input("pi", PI_SHADER_INPUT); foamNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); foamNP.set_shader_input("maskTexture", foamMaskTexture); foamNP.set_shader_input("foamDepth", foamDepth); foamNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); foamNP.set_shader_input("viewWorldMat", currentViewWorldMat); foamNP.set_shader_input("positionFromTexture", positionTexture1); foamNP.set_shader_input("positionToTexture", positionTexture0); foamCamera->set_initial_state(foamNP.get_state()); PT(Texture) foamTexture = foamBuffer->get_texture(); framebufferTextureArguments.name = "reflectionColor"; FramebufferTexture reflectionColorFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) reflectionColorBuffer = reflectionColorFramebufferTexture.buffer; PT(Camera) reflectionColorCamera = reflectionColorFramebufferTexture.camera; NodePath reflectionColorNP = reflectionColorFramebufferTexture.shaderNP; reflectionColorBuffer->set_sort(refractionBuffer->get_sort() + 1); reflectionColorNP.set_shader(reflectionColorShader); reflectionColorNP.set_shader_input("colorTexture", refractionTexture); reflectionColorNP.set_shader_input("uvTexture", reflectionUvTexture); reflectionColorCamera->set_initial_state(reflectionColorNP.get_state()); PT(Texture) reflectionColorTexture = reflectionColorBuffer->get_texture(); framebufferTextureArguments.name = "reflectionColorBlur"; FramebufferTexture reflectionColorBlurFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) reflectionColorBlurBuffer = reflectionColorBlurFramebufferTexture.buffer; PT(Camera) reflectionColorBlurCamera = reflectionColorBlurFramebufferTexture.camera; NodePath reflectionColorBlurNP = reflectionColorBlurFramebufferTexture.shaderNP; reflectionColorBlurBuffer->set_sort(reflectionColorBuffer->get_sort() + 1); reflectionColorBlurNP.set_shader(boxBlurShader); reflectionColorBlurNP.set_shader_input("colorTexture", reflectionColorTexture); reflectionColorBlurNP.set_shader_input("parameters", LVecBase2f(8, 1)); reflectionColorBlurCamera->set_initial_state(reflectionColorBlurNP.get_state()); PT(Texture) reflectionColorBlurTexture = reflectionColorBlurBuffer->get_texture(); framebufferTextureArguments.name = "reflection"; FramebufferTexture reflectionFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) reflectionBuffer = reflectionFramebufferTexture.buffer; NodePath reflectionNP = reflectionFramebufferTexture.shaderNP; reflectionBuffer->set_sort(reflectionColorBlurBuffer->get_sort() + 1); reflectionNP.set_shader(reflectionShader); reflectionNP.set_shader_input("colorTexture", reflectionColorTexture); reflectionNP.set_shader_input("colorBlurTexture", reflectionColorBlurTexture); reflectionNP.set_shader_input("maskTexture", reflectionMaskTexture); reflectionFramebufferTexture.camera->set_initial_state(reflectionNP.get_state()); PT(Texture) reflectionTexture = reflectionBuffer->get_texture(); framebufferTextureArguments.name = "baseCombine"; FramebufferTexture baseCombineFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) baseCombineBuffer = baseCombineFramebufferTexture.buffer; PT(Camera) baseCombineCamera = baseCombineFramebufferTexture.camera; NodePath baseCombineNP = baseCombineFramebufferTexture.shaderNP; baseCombineBuffer->set_sort(reflectionBuffer->get_sort() + 1); baseCombineNP.set_shader(baseCombineShader); baseCombineNP.set_shader_input("baseTexture", baseTexture); baseCombineNP.set_shader_input("refractionTexture", refractionTexture); baseCombineNP.set_shader_input("foamTexture", foamTexture); baseCombineNP.set_shader_input("reflectionTexture", reflectionTexture); baseCombineNP.set_shader_input("specularTexture", specularTexture); baseCombineCamera->set_initial_state(baseCombineNP.get_state()); PT(Texture) baseCombineTexture = baseCombineBuffer->get_texture(); framebufferTextureArguments.name = "sharpen"; FramebufferTexture sharpenFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) sharpenBuffer = sharpenFramebufferTexture.buffer; NodePath sharpenNP = sharpenFramebufferTexture.shaderNP; sharpenBuffer->set_sort(baseCombineBuffer->get_sort() + 1); sharpenNP.set_shader(sharpenShader); sharpenNP.set_shader_input("colorTexture", baseCombineTexture); sharpenNP.set_shader_input("enabled", sharpenEnabled); PT(Camera) sharpenCamera = sharpenFramebufferTexture.camera; sharpenCamera->set_initial_state(sharpenNP.get_state()); PT(Texture) sharpenTexture = sharpenBuffer->get_texture(); framebufferTextureArguments.name = "posterize"; FramebufferTexture posterizeFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) posterizeBuffer = posterizeFramebufferTexture.buffer; NodePath posterizeNP = posterizeFramebufferTexture.shaderNP; posterizeBuffer->set_sort(sharpenBuffer->get_sort() + 1); posterizeNP.set_shader(posterizeShader); posterizeNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); posterizeNP.set_shader_input("colorTexture", sharpenTexture); posterizeNP.set_shader_input("positionTexture", positionTexture2); posterizeNP.set_shader_input("enabled", posterizeEnabled); PT(Camera) posterizeCamera = posterizeFramebufferTexture.camera; posterizeCamera->set_initial_state(posterizeNP.get_state()); PT(Texture) posterizeTexture = posterizeBuffer->get_texture(); framebufferTextureArguments.name = "bloom"; FramebufferTexture bloomFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) bloomBuffer = bloomFramebufferTexture.buffer; PT(Camera) bloomCamera = bloomFramebufferTexture.camera; NodePath bloomNP = bloomFramebufferTexture.shaderNP; bloomBuffer->set_sort(posterizeBuffer->get_sort() + 1); bloomNP.set_shader(bloomShader); bloomNP.set_shader_input("colorTexture", posterizeTexture); bloomNP.set_shader_input("enabled", bloomEnabled); bloomCamera->set_initial_state(bloomNP.get_state()); PT(Texture) bloomTexture = bloomBuffer->get_texture(); framebufferTextureArguments.name = "sceneCombine"; FramebufferTexture sceneCombineFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) sceneCombineBuffer = sceneCombineFramebufferTexture.buffer; PT(Camera) sceneCombineCamera = sceneCombineFramebufferTexture.camera; NodePath sceneCombineNP = sceneCombineFramebufferTexture.shaderNP; sceneCombineBuffer->set_sort(bloomBuffer->get_sort() + 1); sceneCombineNP.set_shader(sceneCombineShader); sceneCombineNP.set_shader_input("pi", PI_SHADER_INPUT); sceneCombineNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); sceneCombineNP.set_shader_input("lookupTableTextureN", colorLookupTableTextureN); sceneCombineNP.set_shader_input("backgroundColor0", backgroundColor[0]); sceneCombineNP.set_shader_input("backgroundColor1", backgroundColor[1]); sceneCombineNP.set_shader_input("baseTexture", posterizeTexture); sceneCombineNP.set_shader_input("bloomTexture", bloomTexture); sceneCombineNP.set_shader_input("fogTexture", fogTexture); sceneCombineNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); PT(Texture) sceneCombineTexture = sceneCombineBuffer->get_texture(); sceneCombineCamera->set_initial_state(sceneCombineNP.get_state()); framebufferTextureArguments.clearColor = backgroundColor[1]; framebufferTextureArguments.name = "outOfFocus"; FramebufferTexture outOfFocusFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) outOfFocusBuffer = outOfFocusFramebufferTexture.buffer; PT(Camera) outOfFocusCamera = outOfFocusFramebufferTexture.camera; NodePath outOfFocusNP = outOfFocusFramebufferTexture.shaderNP; outOfFocusBuffer->set_sort(sceneCombineBuffer->get_sort() + 1); outOfFocusNP.set_shader(boxBlurShader); outOfFocusNP.set_shader_input("colorTexture", sceneCombineTexture); outOfFocusNP.set_shader_input("parameters", LVecBase2f(2, 2)); outOfFocusCamera->set_initial_state(outOfFocusNP.get_state()); PT(Texture) outOfFocusTexture = outOfFocusBuffer->get_texture(); framebufferTextureArguments.name = "dilatedOutOfFocus"; FramebufferTexture dilatedOutOfFocusFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) dilatedOutOfFocusBuffer = dilatedOutOfFocusFramebufferTexture.buffer; PT(Camera) dilatedOutOfFocusCamera = dilatedOutOfFocusFramebufferTexture.camera; NodePath dilatedOutOfFocusNP = dilatedOutOfFocusFramebufferTexture.shaderNP; dilatedOutOfFocusBuffer->set_sort(outOfFocusBuffer->get_sort() + 1); dilatedOutOfFocusNP.set_shader(dilationShader); dilatedOutOfFocusNP.set_shader_input("colorTexture", outOfFocusTexture); dilatedOutOfFocusNP.set_shader_input("parameters", LVecBase2f(4, 2)); dilatedOutOfFocusCamera->set_initial_state(dilatedOutOfFocusNP.get_state()); PT(Texture) dilatedOutOfFocusTexture = dilatedOutOfFocusBuffer->get_texture(); framebufferTextureArguments.aux_rgba = 1; framebufferTextureArguments.name = "depthOfField"; FramebufferTexture depthOfFieldFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) depthOfFieldBuffer = depthOfFieldFramebufferTexture.buffer; NodePath depthOfFieldNP = depthOfFieldFramebufferTexture.shaderNP; depthOfFieldBuffer->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , GraphicsOutput::RTP_aux_rgba_0 ); depthOfFieldBuffer->set_clear_active(3, true); depthOfFieldBuffer->set_clear_value( 3, framebufferTextureArguments.clearColor); depthOfFieldBuffer->set_sort(dilatedOutOfFocusBuffer->get_sort() + 1); depthOfFieldNP.set_shader(depthOfFieldShader); depthOfFieldNP.set_shader_input("positionTexture", positionTexture0); depthOfFieldNP.set_shader_input("focusTexture", sceneCombineTexture); depthOfFieldNP.set_shader_input("outOfFocusTexture", dilatedOutOfFocusTexture); depthOfFieldNP.set_shader_input("mouseFocusPoint", mouseFocusPoint); depthOfFieldNP.set_shader_input("nearFar", cameraNearFar); depthOfFieldNP.set_shader_input("enabled", depthOfFieldEnabled); PT(Camera) depthOfFieldCamera = depthOfFieldFramebufferTexture.camera; depthOfFieldCamera->set_initial_state(depthOfFieldNP.get_state()); PT(Texture) depthOfFieldTexture0 = depthOfFieldBuffer->get_texture(0); PT(Texture) depthOfFieldTexture1 = depthOfFieldBuffer->get_texture(1); framebufferTextureArguments.aux_rgba = 0; framebufferTextureArguments.name = "outline"; FramebufferTexture outlineFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) outlineBuffer = outlineFramebufferTexture.buffer; PT(Camera) outlineCamera = outlineFramebufferTexture.camera; NodePath outlineNP = outlineFramebufferTexture.shaderNP; outlineBuffer->set_sort(depthOfFieldBuffer->get_sort() + 1); outlineNP.set_shader(outlineShader); outlineNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); outlineNP.set_shader_input("positionTexture", positionTexture0); outlineNP.set_shader_input("colorTexture", depthOfFieldTexture0); outlineNP.set_shader_input("noiseTexture", colorNoiseTexture); outlineNP.set_shader_input("depthOfFieldTexture", depthOfFieldTexture1); outlineNP.set_shader_input("fogTexture", fogTexture); outlineNP.set_shader_input("nearFar", cameraNearFar); outlineNP.set_shader_input("enabled", outlineEnabled); outlineCamera->set_initial_state(outlineNP.get_state()); PT(Texture) outlineTexture = outlineBuffer->get_texture(); framebufferTextureArguments.name = "painterly"; FramebufferTexture painterlyFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) painterlyBuffer = painterlyFramebufferTexture.buffer; NodePath painterlyNP = painterlyFramebufferTexture.shaderNP; painterlyBuffer->set_sort(outlineBuffer->get_sort() + 1); painterlyNP.set_shader(kuwaharaFilterShader); painterlyNP.set_shader_input("colorTexture", outlineTexture); painterlyNP.set_shader_input("parameters", LVecBase2f(0, 0)); PT(Camera) painterlyCamera = painterlyFramebufferTexture.camera; painterlyCamera->set_initial_state(painterlyNP.get_state()); PT(Texture) painterlyTexture = painterlyBuffer->get_texture(); framebufferTextureArguments.name = "pixelize"; FramebufferTexture pixelizeFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) pixelizeBuffer = pixelizeFramebufferTexture.buffer; NodePath pixelizeNP = pixelizeFramebufferTexture.shaderNP; pixelizeBuffer->set_sort(painterlyBuffer->get_sort() + 1); pixelizeNP.set_shader(pixelizeShader); pixelizeNP.set_shader_input("colorTexture", painterlyTexture); pixelizeNP.set_shader_input("positionTexture", positionTexture2); pixelizeNP.set_shader_input("parameters", LVecBase2f(5, 0)); pixelizeNP.set_shader_input("enabled", pixelizeEnabled); PT(Camera) pixelizeCamera = pixelizeFramebufferTexture.camera; pixelizeCamera->set_initial_state(pixelizeNP.get_state()); PT(Texture) pixelizeTexture = pixelizeBuffer->get_texture(); framebufferTextureArguments.name = "motionBlur"; FramebufferTexture motionBlurFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) motionBlurBuffer = motionBlurFramebufferTexture.buffer; NodePath motionBlurNP = motionBlurFramebufferTexture.shaderNP; motionBlurBuffer->set_sort(pixelizeBuffer->get_sort() + 1); motionBlurNP.set_shader(motionBlurShader); motionBlurNP.set_shader_input("previousViewWorldMat", previousViewWorldMat); motionBlurNP.set_shader_input("worldViewMat", render.get_transform(cameraNP)->get_mat()); motionBlurNP.set_shader_input("lensProjection", geometryCameraLens2->get_projection_mat()); motionBlurNP.set_shader_input("positionTexture", positionTexture2); motionBlurNP.set_shader_input("colorTexture", pixelizeTexture); motionBlurNP.set_shader_input("motionBlurEnabled", motionBlurEnabled); motionBlurNP.set_shader_input("parameters", LVecBase2f(2, 1.0)); PT(Camera) motionBlurCamera = motionBlurFramebufferTexture.camera; motionBlurCamera->set_initial_state(motionBlurNP.get_state()); PT(Texture) motionBlurTexture = motionBlurBuffer->get_texture(); framebufferTextureArguments.name = "filmGrain"; FramebufferTexture filmGrainFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) filmGrainBuffer = filmGrainFramebufferTexture.buffer; NodePath filmGrainNP = filmGrainFramebufferTexture.shaderNP; filmGrainBuffer->set_sort(motionBlurBuffer->get_sort() + 1); filmGrainNP.set_shader(filmGrainShader); filmGrainNP.set_shader_input("pi", PI_SHADER_INPUT); filmGrainNP.set_shader_input("colorTexture", motionBlurTexture); filmGrainNP.set_shader_input("enabled", filmGrainEnabled); PT(Camera) filmGrainCamera = filmGrainFramebufferTexture.camera; filmGrainCamera->set_initial_state(filmGrainNP.get_state()); PT(Texture) filmGrainTexture = filmGrainBuffer->get_texture(); framebufferTextureArguments.name = "lookupTable"; FramebufferTexture lookupTableFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) lookupTableBuffer = lookupTableFramebufferTexture.buffer; NodePath lookupTableNP = lookupTableFramebufferTexture.shaderNP; lookupTableBuffer->set_sort(filmGrainBuffer->get_sort() + 1); lookupTableNP.set_shader(lookupTableShader); lookupTableNP.set_shader_input("pi", PI_SHADER_INPUT); lookupTableNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); lookupTableNP.set_shader_input("colorTexture", filmGrainTexture); lookupTableNP.set_shader_input("lookupTableTextureN", colorLookupTableTextureN); lookupTableNP.set_shader_input("lookupTableTexture0", colorLookupTableTexture0); lookupTableNP.set_shader_input("lookupTableTexture1", colorLookupTableTexture1); lookupTableNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); lookupTableNP.set_shader_input("enabled", lookupTableEnabled); PT(Camera) lookupTableCamera = lookupTableFramebufferTexture.camera; lookupTableCamera->set_initial_state(lookupTableNP.get_state()); PT(Texture) lookupTableTexture = lookupTableBuffer->get_texture(); framebufferTextureArguments.name = "gammaCorrection"; FramebufferTexture gammaCorrectionFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) gammaCorrectionBuffer = gammaCorrectionFramebufferTexture.buffer; NodePath gammaCorrectionNP = gammaCorrectionFramebufferTexture.shaderNP; gammaCorrectionBuffer->set_sort(lookupTableBuffer->get_sort() + 1); gammaCorrectionNP.set_shader(gammaCorrectionShader); gammaCorrectionNP.set_shader_input("gamma", GAMMA_SHADER_INPUT); gammaCorrectionNP.set_shader_input("colorTexture", lookupTableTexture); PT(Camera) gammaCorrectionCamera = gammaCorrectionFramebufferTexture.camera; gammaCorrectionCamera->set_initial_state(gammaCorrectionNP.get_state()); PT(Texture) gammaCorrectionTexture = gammaCorrectionBuffer->get_texture(); framebufferTextureArguments.name = "chromaticAberration"; FramebufferTexture chromaticAberrationFramebufferTexture = generateFramebufferTexture ( framebufferTextureArguments ); PT(GraphicsOutput) chromaticAberrationBuffer = chromaticAberrationFramebufferTexture.buffer; NodePath chromaticAberrationNP = chromaticAberrationFramebufferTexture.shaderNP; chromaticAberrationBuffer->set_sort(gammaCorrectionBuffer->get_sort() + 1); chromaticAberrationNP.set_shader(chromaticAberrationShader); chromaticAberrationNP.set_shader_input("mouseFocusPoint", mouseFocusPoint); chromaticAberrationNP.set_shader_input("colorTexture", gammaCorrectionTexture); chromaticAberrationNP.set_shader_input("enabled", chromaticAberrationEnabled); PT(Camera) chromaticAberrationCamera = chromaticAberrationFramebufferTexture.camera; chromaticAberrationCamera->set_initial_state(chromaticAberrationNP.get_state()); graphicsOutput->set_sort(chromaticAberrationBuffer->get_sort() + 1); int showBufferIndex = 0; std::vector> bufferArray = { std::make_tuple("Positions 0", geometryBuffer0, 0) , std::make_tuple("Normals 0", geometryBuffer0, 1) , std::make_tuple("Positions 1", geometryBuffer1, 0) , std::make_tuple("Normals 1", geometryBuffer1, 1) , std::make_tuple("Reflection Mask", geometryBuffer1, 2) , std::make_tuple("Refraction Mask", geometryBuffer1, 3) , std::make_tuple("Foam Mask", geometryBuffer1, 4) , std::make_tuple("Positions 2", geometryBuffer2, 0) , std::make_tuple("Smoke Mask", geometryBuffer2, 1) , std::make_tuple("SSAO", ssaoBuffer, 0) , std::make_tuple("SSAO Blur", ssaoBlurBuffer, 0) , std::make_tuple("Refraction UV", refractionUvBuffer, 0) , std::make_tuple("Refraction", refractionBuffer, 0) , std::make_tuple("Reflection UV", reflectionUvBuffer, 0) , std::make_tuple("Reflection Color", reflectionColorBuffer, 0) , std::make_tuple("Reflection Blur", reflectionColorBlurBuffer, 0) , std::make_tuple("Reflection", reflectionBuffer, 0) , std::make_tuple("Foam", foamBuffer, 0) , std::make_tuple("Base", baseBuffer, 0) , std::make_tuple("Specular", baseBuffer, 1) , std::make_tuple("Base Combine", baseCombineBuffer, 0) , std::make_tuple("Painterly", painterlyBuffer, 0) , std::make_tuple("Posterize", posterizeBuffer, 0) , std::make_tuple("Bloom", bloomBuffer, 0) , std::make_tuple("Outline", outlineBuffer, 0) , std::make_tuple("Fog", fogBuffer, 0) , std::make_tuple("Scene Combine", sceneCombineBuffer, 0) , std::make_tuple("Out of Focus", outOfFocusBuffer, 0) , std::make_tuple("Dilation", dilatedOutOfFocusBuffer, 0) , std::make_tuple("Depth of Field Blur", depthOfFieldBuffer, 1) , std::make_tuple("Depth of Field", depthOfFieldBuffer, 0) , std::make_tuple("Pixelize", pixelizeBuffer, 0) , std::make_tuple("Motion Blur", motionBlurBuffer, 0) , std::make_tuple("Film Grain", filmGrainBuffer, 0) , std::make_tuple("Lookup Table", lookupTableBuffer, 0) , std::make_tuple("Gamma Correction", gammaCorrectionBuffer, 0) , std::make_tuple("Chromatic Aberration", chromaticAberrationBuffer, 0) }; showBufferIndex = bufferArray.size() - 1; showBuffer ( render2d , statusNP , bufferArray[showBufferIndex] , false ); shuttersAnimationCollection.play( "close-shutters" ); weatherVaneAnimationCollection.loop("weather-vane-shake", true); bannerAnimationCollection.loop( "banner-swing", true); int then = microsecondsSinceEpoch(); int loopStartedAt = then; int now = then; int keyTime = now; auto beforeFrame = [&]() -> void { WindowProperties windowProperties = graphicsWindow->get_properties(); if (windowProperties.get_minimized()) { std::this_thread::sleep_for(std::chrono::seconds(1)); } now = microsecondsSinceEpoch(); // Avoids a loud audio pop. if (!soundStarted && microsecondToSecond(now - loopStartedAt) >= startSoundAt) { for_each ( sounds.begin() , sounds.end() , [](PT(AudioSound) sound) { sound->set_loop(true); sound->play(); } ); soundStarted = true; } double delta = microsecondToSecond(now - then); then = now; double movement = 100 * delta; double timeSinceKey = microsecondToSecond(now - keyTime); bool keyDebounced = timeSinceKey >= 0.2; double cameraUpDownAdjust = 0; double cameraLeftRightAdjust = 0; bool shiftDown = isButtonDown(mouseWatcher, "shift"); bool tabDown = isButtonDown(mouseWatcher, "tab"); bool resetDown = isButtonDown(mouseWatcher, "r"); bool fogNearDown = isButtonDown(mouseWatcher, "["); bool fogFarDown = isButtonDown(mouseWatcher, "]"); bool equalDown = isButtonDown(mouseWatcher, "="); bool minusDown = isButtonDown(mouseWatcher, "-"); bool deleteDown = isButtonDown(mouseWatcher, "delete"); bool wDown = isButtonDown(mouseWatcher, "w"); bool aDown = isButtonDown(mouseWatcher, "a"); bool dDown = isButtonDown(mouseWatcher, "d"); bool sDown = isButtonDown(mouseWatcher, "s"); bool zDown = isButtonDown(mouseWatcher, "z"); bool xDown = isButtonDown(mouseWatcher, "x"); bool arrowUpDown = isButtonDown(mouseWatcher, "arrow_up"); bool arrowDownDown = isButtonDown(mouseWatcher, "arrow_down"); bool arrowLeftDown = isButtonDown(mouseWatcher, "arrow_left"); bool arrowRightDown = isButtonDown(mouseWatcher, "arrow_right"); bool middayDown = isButtonDown(mouseWatcher, "1"); bool midnightDown = isButtonDown(mouseWatcher, "2"); bool fresnelDown = isButtonDown(mouseWatcher, "3"); bool rimLightDown = isButtonDown(mouseWatcher, "4"); bool particlesDown = isButtonDown(mouseWatcher, "5"); bool motionBlurDown = isButtonDown(mouseWatcher, "6"); bool painterlyDown = isButtonDown(mouseWatcher, "7"); bool celShadingDown = isButtonDown(mouseWatcher, "8"); bool lookupTableDown = isButtonDown(mouseWatcher, "9"); bool blinnPhongDown = isButtonDown(mouseWatcher, "0"); bool ssaoDown = isButtonDown(mouseWatcher, "y"); bool outlineDown = isButtonDown(mouseWatcher, "u"); bool bloomDown = isButtonDown(mouseWatcher, "i"); bool normalMapsDown = isButtonDown(mouseWatcher, "o"); bool fogDown = isButtonDown(mouseWatcher, "p"); bool depthOfFieldDown = isButtonDown(mouseWatcher, "h"); bool posterizeDown = isButtonDown(mouseWatcher, "j"); bool pixelizeDown = isButtonDown(mouseWatcher, "k"); bool sharpenDown = isButtonDown(mouseWatcher, "l"); bool filmGrainDown = isButtonDown(mouseWatcher, "n"); bool reflectionDown = isButtonDown(mouseWatcher, "m"); bool refractionDown = isButtonDown(mouseWatcher, ","); bool flowMapsDown = isButtonDown(mouseWatcher, "."); bool sunlightDown = isButtonDown(mouseWatcher, "/"); bool chromaticAberrationDown = isButtonDown(mouseWatcher, "\\"); bool mouseLeftDown = mouseWatcher->is_button_down(MouseButton::one()); bool mouseMiddleDown = mouseWatcher->is_button_down(MouseButton::two()); bool mouseRightDown = mouseWatcher->is_button_down(MouseButton::three()); if (wDown) { cameraRotatePhi -= movement * 0.5; } if (sDown) { cameraRotatePhi += movement * 0.5; } if (aDown) { cameraRotateTheta += movement * 0.5; } if (dDown) { cameraRotateTheta -= movement * 0.5; } if (zDown || mouseWheelUp) { cameraRotateRadius -= movement * 4 + 50 * mouseWheelUp; mouseWheelUp = false; } if (xDown || mouseWheelDown) { cameraRotateRadius += movement * 4 + 50 * mouseWheelDown; mouseWheelDown = false; } if (cameraRotatePhi < 1) cameraRotatePhi = 1; if (cameraRotatePhi > 179) cameraRotatePhi = 179; if (cameraRotatePhi < 0) cameraRotatePhi = 360 - cameraRotateTheta; if (cameraRotateTheta > 360) cameraRotateTheta = cameraRotateTheta - 360; if (cameraRotateTheta < 0) cameraRotateTheta = 360 - cameraRotateTheta; if (cameraRotateRadius < cameraNear + 5) cameraRotateRadius = cameraNear + 5; if (cameraRotateRadius > cameraFar - 10) cameraRotateRadius = cameraFar - 10; if (arrowUpDown) { cameraUpDownAdjust = -2 * delta; } else if (arrowDownDown) { cameraUpDownAdjust = 2 * delta; } if (arrowLeftDown) { cameraLeftRightAdjust = 2 * delta; } else if (arrowRightDown) { cameraLeftRightAdjust = -2 * delta; } if (mouseWatcher->has_mouse()) { mouseNow[0] = mouseWatcher->get_mouse_x(); mouseNow[1] = mouseWatcher->get_mouse_y(); if (mouseLeftDown) { cameraRotateTheta += (mouseThen[0] - mouseNow[0] ) * movement; cameraRotatePhi += (mouseNow[1] - mouseThen[1]) * movement; } else if (mouseRightDown) { cameraLeftRightAdjust = (mouseThen[0] - mouseNow[0]) * movement; cameraUpDownAdjust = (mouseThen[1] - mouseNow[1]) * movement; } else if (mouseMiddleDown) { mouseFocusPoint = LVecBase2f ( (mouseNow[0] + 1.0) / 2.0 , (mouseNow[1] + 1.0) / 2.0 ); } if (!mouseLeftDown) { mouseThen = mouseNow; } } if (shiftDown && fogNearDown) { fogNear += fogAdjust; statusAlpha = 1.0; statusText = "Fog Near " + std::to_string(fogNear); } else if (fogNearDown) { fogNear -= fogAdjust; statusAlpha = 1.0; statusText = "Fog Near " + std::to_string(fogNear); } if (shiftDown && fogFarDown) { fogFar -= fogAdjust; statusAlpha = 1.0; statusText = "Fog Far " + std::to_string(fogFar); } else if (fogFarDown) { fogFar += fogAdjust; statusAlpha = 1.0; statusText = "Fog Far " + std::to_string(fogFar); } if (shiftDown && equalDown) { rior[0] -= riorAdjust; statusAlpha = 1.0; statusText = "Refractive Index " + std::to_string(rior[0]); } else if (equalDown) { rior[0] += riorAdjust; statusAlpha = 1.0; statusText = "Refractive Index " + std::to_string(rior[0]); } rior[1] = rior[0]; if (shiftDown && minusDown) { foamDepth[0] -= foamDepthAdjust; if (foamDepth[0] < 0.001) { foamDepth[0] = 0.001; }; statusAlpha = 1.0; statusText = "Foam Depth " + std::to_string(foamDepth[0]); } else if (minusDown) { foamDepth[0] += foamDepthAdjust; statusAlpha = 1.0; statusText = "Foam Depth " + std::to_string(foamDepth[0]); } foamDepth[1] = foamDepth[0]; if (keyDebounced) { if (tabDown) { if (shiftDown) { showBufferIndex -= 1; if (showBufferIndex < 0) showBufferIndex = bufferArray.size() - 1; } else { showBufferIndex += 1; if (showBufferIndex >= bufferArray.size()) showBufferIndex = 0; } std::string bufferName = std::get<0>(bufferArray[showBufferIndex]); bool showAlpha = bufferName == "Outline" || bufferName == "Foam" || bufferName == "Fog" ; showBuffer ( render2d , statusNP , bufferArray[showBufferIndex] , showAlpha ); keyTime = now; statusAlpha = 1.0; statusText = bufferName + " Buffer"; } if (resetDown) { cameraRotateRadius = cameraRotateRadiusInitial; cameraRotatePhi = cameraRotatePhiInitial; cameraRotateTheta = cameraRotateThetaInitial; cameraLookAt = cameraLookAtInitial; fogNear = fogNearInitial; fogFar = fogFarInitial; foamDepth = foamDepthInitial; rior = riorInitial; mouseFocusPoint = mouseFocusPointInitial; keyTime = now; statusAlpha = 1.0; statusText = "Reset"; } auto toggleStatus = [&](LVecBase2f enabled, std::string effect) -> void { statusAlpha = 1.0; if (enabled[0] == 1) { statusText = effect + " On"; } else { statusText = effect + " Off"; } }; if (ssaoDown) { ssaoEnabled = toggleEnabledVec(ssaoEnabled); keyTime = now; toggleStatus ( ssaoEnabled , "SSAO" ); } if (refractionDown) { refractionEnabled = toggleEnabledVec(refractionEnabled); keyTime = now; toggleStatus ( refractionEnabled , "Refraction" ); } if (reflectionDown) { reflectionEnabled = toggleEnabledVec(reflectionEnabled); keyTime = now; toggleStatus ( reflectionEnabled , "Reflection" ); } if (bloomDown) { bloomEnabled = toggleEnabledVec(bloomEnabled); keyTime = now; toggleStatus ( bloomEnabled , "Bloom" ); } if (normalMapsDown){ normalMapsEnabled = toggleEnabledVec(normalMapsEnabled); keyTime = now; toggleStatus ( normalMapsEnabled , "Normal Maps" ); } if (fogDown) { fogEnabled = toggleEnabledVec(fogEnabled); keyTime = now; toggleStatus ( fogEnabled , "Fog" ); } if (outlineDown) { outlineEnabled = toggleEnabledVec(outlineEnabled); keyTime = now; toggleStatus ( outlineEnabled , "Outline" ); } if (celShadingDown) { celShadingEnabled = toggleEnabledVec(celShadingEnabled); keyTime = now; toggleStatus ( celShadingEnabled , "Cel Shading" ); } if (lookupTableDown) { lookupTableEnabled = toggleEnabledVec(lookupTableEnabled); keyTime = now; toggleStatus ( lookupTableEnabled , "Lookup Table" ); } if (fresnelDown) { fresnelEnabled = toggleEnabledVec(fresnelEnabled); keyTime = now; toggleStatus ( fresnelEnabled , "Fresnel" ); } if (rimLightDown) { rimLightEnabled = toggleEnabledVec(rimLightEnabled); keyTime = now; toggleStatus ( rimLightEnabled , "Rim Light" ); } if (blinnPhongDown) { blinnPhongEnabled = toggleEnabledVec(blinnPhongEnabled); keyTime = now; toggleStatus ( blinnPhongEnabled , "Blinn-Phong" ); } if (sharpenDown) { sharpenEnabled = toggleEnabledVec(sharpenEnabled); keyTime = now; toggleStatus ( sharpenEnabled , "Sharpen" ); } if (depthOfFieldDown) { depthOfFieldEnabled = toggleEnabledVec(depthOfFieldEnabled); keyTime = now; toggleStatus ( depthOfFieldEnabled , "Depth of Field" ); } if (painterlyDown) { painterlyEnabled = toggleEnabledVec(painterlyEnabled); keyTime = now; toggleStatus ( painterlyEnabled , "Painterly" ); } if (motionBlurDown) { motionBlurEnabled = toggleEnabledVec(motionBlurEnabled); keyTime = now; toggleStatus ( motionBlurEnabled , "Motion Blur" ); } if (posterizeDown) { posterizeEnabled = toggleEnabledVec(posterizeEnabled); keyTime = now; toggleStatus ( posterizeEnabled , "Posterize" ); } if (pixelizeDown) { pixelizeEnabled = toggleEnabledVec(pixelizeEnabled); keyTime = now; toggleStatus ( pixelizeEnabled , "Pixelize" ); } if (filmGrainDown) { filmGrainEnabled = toggleEnabledVec(filmGrainEnabled); keyTime = now; toggleStatus ( filmGrainEnabled , "Film Grain" ); } if (flowMapsDown) { flowMapsEnabled = toggleEnabledVec(flowMapsEnabled); if (flowMapsEnabled[0] == 1 && soundEnabled) { for_each(sounds.begin(), sounds.end(), setSoundOn); } else if (flowMapsEnabled[0] != 1) { for_each(sounds.begin(), sounds.end(), setSoundOff); } keyTime = now; toggleStatus ( flowMapsEnabled , "Flow Maps" ); } if (deleteDown) { if (soundEnabled) { for_each(sounds.begin(), sounds.end(), setSoundOff); soundEnabled = false; } else { if (flowMapsEnabled[0] == 1) { for_each(sounds.begin(), sounds.end(), setSoundOn); } soundEnabled = true; } keyTime = now; toggleStatus ( LVecBase2f(soundEnabled ? 1 : 0, 0) , "Sound" ); } if (sunlightDown) { animateSunlight = animateSunlight ? false : true; keyTime = now; toggleStatus ( LVecBase2f(animateSunlight ? 1 : 0, 0) , "Sun Animation" ); } if (particlesDown) { keyTime = now; statusAlpha = 1.0; if (smokeNP.is_hidden()) { smokeNP.show(); statusText = "Particles On"; } else { smokeNP.hide(); statusText = "Particles Off"; } } if (chromaticAberrationDown) { chromaticAberrationEnabled = toggleEnabledVec(chromaticAberrationEnabled); keyTime = now; toggleStatus ( chromaticAberrationEnabled , "Chromatic Aberration" ); } } if (flowMapsEnabled[0]) { float wheelP = wheelNP.get_p(); wheelP += -90.0 * delta; if (wheelP > 360) wheelP = 0; if (wheelP < 0) wheelP = 360; wheelNP.set_p(wheelP); } if (animateSunlight || middayDown || midnightDown) { sunlightP = animateLights ( render , shuttersAnimationCollection , delta , -360.0 / 64.0 , closedShutters , middayDown , midnightDown ); if (middayDown) { statusAlpha = 1.0; statusText = "Midday"; } else if (midnightDown) { statusAlpha = 1.0; statusText = "Midnight"; } } cameraLookAt = calculateCameraLookAt ( cameraUpDownAdjust , cameraLeftRightAdjust , cameraRotatePhi , cameraRotateTheta , cameraLookAt ); cameraNP.set_pos ( calculateCameraPosition ( cameraRotateRadius , cameraRotatePhi , cameraRotateTheta , cameraLookAt ) ); cameraNP.look_at(cameraLookAt); currentViewWorldMat = cameraNP.get_transform(render)->get_mat(); geometryNP0.set_shader_input("normalMapsEnabled", normalMapsEnabled); geometryNP0.set_shader_input("flowMapsEnabled", flowMapsEnabled); geometryCamera0->set_initial_state(geometryNP0.get_state()); geometryNP1.set_shader_input("normalMapsEnabled", normalMapsEnabled); geometryNP1.set_shader_input("flowMapsEnabled", flowMapsEnabled); geometryCamera1->set_initial_state(geometryNP1.get_state()); fogNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); fogNP.set_shader_input("origin", cameraNP.get_relative_point(render, environmentNP.get_pos())); fogNP.set_shader_input("nearFar", LVecBase2f(fogNear, fogFar)); fogNP.set_shader_input("enabled", fogEnabled); fogCamera->set_initial_state(fogNP.get_state()); ssaoNP.set_shader_input("lensProjection", geometryCameraLens0->get_projection_mat()); ssaoNP.set_shader_input("enabled", ssaoEnabled); ssaoCamera->set_initial_state(ssaoNP.get_state()); refractionUvNP.set_shader_input("lensProjection", geometryCameraLens1->get_projection_mat()); refractionUvNP.set_shader_input("enabled", refractionEnabled); refractionUvNP.set_shader_input("rior", rior); refractionUvCamera->set_initial_state(refractionUvNP.get_state()); reflectionUvNP.set_shader_input("lensProjection", geometryCameraLens1->get_projection_mat()); reflectionUvNP.set_shader_input("enabled", reflectionEnabled); reflectionUvCamera->set_initial_state(reflectionUvNP.get_state()); foamNP.set_shader_input("foamDepth", foamDepth); foamNP.set_shader_input("viewWorldMat", currentViewWorldMat); foamNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); foamCamera->set_initial_state(foamNP.get_state()); bloomNP.set_shader_input("enabled", bloomEnabled); bloomCamera->set_initial_state(bloomNP.get_state()); outlineNP.set_shader_input("enabled", outlineEnabled); outlineCamera->set_initial_state(outlineNP.get_state()); baseNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); baseNP.set_shader_input("normalMapsEnabled", normalMapsEnabled); baseNP.set_shader_input("blinnPhongEnabled", blinnPhongEnabled); baseNP.set_shader_input("fresnelEnabled", fresnelEnabled); baseNP.set_shader_input("rimLightEnabled", rimLightEnabled); baseNP.set_shader_input("celShadingEnabled", celShadingEnabled); baseNP.set_shader_input("flowMapsEnabled", flowMapsEnabled); baseCamera->set_initial_state(baseNP.get_state()); refractionNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); refractionCamera->set_initial_state(refractionNP.get_state()); sharpenNP.set_shader_input("enabled", sharpenEnabled); sharpenCamera->set_initial_state(sharpenNP.get_state()); sceneCombineNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); sceneCombineCamera->set_initial_state(sceneCombineNP.get_state()); depthOfFieldNP.set_shader_input("mouseFocusPoint", mouseFocusPoint); depthOfFieldNP.set_shader_input("enabled", depthOfFieldEnabled); depthOfFieldCamera->set_initial_state(depthOfFieldNP.get_state()); painterlyNP.set_shader_input("parameters", LVecBase2f(painterlyEnabled[0] == 1 ? 3 : 0, 0)); painterlyCamera->set_initial_state(painterlyNP.get_state()); motionBlurNP.set_shader_input("previousViewWorldMat", previousViewWorldMat); motionBlurNP.set_shader_input("worldViewMat", render.get_transform(cameraNP)->get_mat()); motionBlurNP.set_shader_input("lensProjection", geometryCameraLens1->get_projection_mat()); motionBlurNP.set_shader_input("motionBlurEnabled", motionBlurEnabled); motionBlurCamera->set_initial_state(motionBlurNP.get_state()); posterizeNP.set_shader_input("enabled", posterizeEnabled); posterizeCamera->set_initial_state(posterizeNP.get_state()); pixelizeNP.set_shader_input("enabled", pixelizeEnabled); pixelizeCamera->set_initial_state(pixelizeNP.get_state()); filmGrainNP.set_shader_input("enabled", filmGrainEnabled); filmGrainCamera->set_initial_state(filmGrainNP.get_state()); lookupTableNP.set_shader_input("enabled", lookupTableEnabled); lookupTableNP.set_shader_input("sunPosition", LVecBase2f(sunlightP, 0)); lookupTableCamera->set_initial_state(lookupTableNP.get_state()); chromaticAberrationNP.set_shader_input("mouseFocusPoint", mouseFocusPoint); chromaticAberrationNP.set_shader_input("enabled", chromaticAberrationEnabled); chromaticAberrationCamera->set_initial_state(chromaticAberrationNP.get_state()); previousViewWorldMat = currentViewWorldMat; statusAlpha = statusAlpha - ((1.0 / statusFadeRate) * delta); statusAlpha = statusAlpha < 0.0 ? 0.0 : statusAlpha; statusColor[3] = statusAlpha; statusShadowColor[3] = statusAlpha; status->set_text_color(statusColor); status->set_shadow_color(statusShadowColor); status->set_text(statusText); updateAudoManager ( sceneRootNP , cameraNP ); particleSystemManager.do_particles(delta); physicsManager.do_physics(delta); }; auto beforeFrameRunner = [](GenericAsyncTask* task, void* arg) -> AsyncTask::DoneStatus { (*static_cast(arg))(); return AsyncTask::DS_cont; }; taskManager->add ( new GenericAsyncTask ( "beforeFrame" , beforeFrameRunner , &beforeFrame ) ); auto setMouseWheelUp = [&]() { mouseWheelUp = true; }; auto setMouseWheelDown = [&]() { mouseWheelDown = true; }; framework.define_key ( "wheel_up" , "Mouse Wheel Up" , [](const Event*, void* arg) { (*static_cast(arg))(); } , &setMouseWheelUp ); framework.define_key ( "wheel_down" , "Mouse Wheel Down" , [](const Event*, void* arg) { (*static_cast(arg))(); } , &setMouseWheelDown ); physicsManager.attach_linear_integrator ( new LinearEulerIntegrator() ); LVector3f wheelNPRelPos = wheelNP.get_pos(sceneRootNP); sounds[0]->set_3d_attributes ( wheelNPRelPos[0] , wheelNPRelPos[1] , wheelNPRelPos[2] , 0 , 0 , 0 ); LVector3f waterNPRelPos = waterNP.get_pos(sceneRootNP); sounds[1]->set_3d_attributes ( waterNPRelPos[0] , waterNPRelPos[1] , waterNPRelPos[2] , 0 , 0 , 0 ); sounds[0]->set_3d_min_distance(60); sounds[1]->set_3d_min_distance(50); framework.main_loop(); audioManager->shutdown(); framework.close_framework(); return 0; } // END MAIN void generateLights ( NodePath render , bool showLights ) { PT(AmbientLight) ambientLight = new AmbientLight("ambientLight"); ambientLight->set_color ( LVecBase4 ( 0.388 , 0.356 , 0.447 , 1 ) ); NodePath ambientLightNP = render.attach_new_node(ambientLight); render.set_light(ambientLightNP); PT(DirectionalLight) sunlight = new DirectionalLight("sunlight"); sunlight->set_color(sunlightColor1); sunlight->set_shadow_caster(true, SHADOW_SIZE, SHADOW_SIZE); sunlight->get_lens()->set_film_size(35, 35); sunlight->get_lens()->set_near_far(5.0, 35.0); if (showLights) sunlight->show_frustum(); NodePath sunlightNP = render.attach_new_node(sunlight); sunlightNP.set_name("sunlight"); render.set_light(sunlightNP); PT(DirectionalLight) moonlight = new DirectionalLight("moonlight"); moonlight->set_color(moonlightColor1); moonlight->set_shadow_caster(true, SHADOW_SIZE, SHADOW_SIZE); moonlight->get_lens()->set_film_size(35, 35); moonlight->get_lens()->set_near_far(5.0, 35); if (showLights) moonlight->show_frustum(); NodePath moonlightNP = render.attach_new_node(moonlight); moonlightNP.set_name("moonlight"); render.set_light_off(moonlightNP); NodePath sunlightPivotNP = NodePath("sunlightPivot"); sunlightPivotNP.reparent_to(render); sunlightPivotNP.set_pos(0, 0.5, 15.0); sunlightNP.reparent_to(sunlightPivotNP); sunlightNP.set_pos(0, -17.5, 0); sunlightPivotNP.set_hpr(135, 340, 0); NodePath moonlightPivotNP = NodePath("moonlightPivot"); moonlightPivotNP.reparent_to(render); moonlightPivotNP.set_pos(0, 0.5, 15.0); moonlightNP.reparent_to(moonlightPivotNP); moonlightNP.set_pos(0, -17.5, 0); moonlightPivotNP.set_hpr(135, 160, 0); generateWindowLight ( "windowLight" , render , LVecBase3 ( 1.5 , 2.49 , 7.9 ) , showLights ); generateWindowLight ( "windowLight1" , render , LVecBase3 ( 3.5 , 2.49 , 7.9 ) , showLights ); generateWindowLight ( "windowLight2" , render , LVecBase3 ( 3.5 , 1.49 , 4.5 ) , showLights ); } void generateWindowLight ( std::string name , NodePath render , LVecBase3 position , bool show ) { PT(Spotlight) windowLight = new Spotlight(name); windowLight->set_color(windowLightColor); windowLight->set_exponent(5); windowLight->set_attenuation(LVecBase3(1, 0.008, 0)); windowLight->set_max_distance(37); PT(PerspectiveLens) windowLightLens = new PerspectiveLens(); windowLightLens->set_near_far(0.5, 12); windowLightLens->set_fov(140); windowLight->set_lens(windowLightLens); if (show) windowLight->show_frustum(); NodePath windowLightNP = render.attach_new_node(windowLight); windowLightNP.set_name(name); windowLightNP.set_pos(position); windowLightNP.set_hpr(180, 0, 0); render.set_light(windowLightNP); } float animateLights ( NodePath render , AnimControlCollection shuttersAnimationCollection , float delta , float speed , bool& closedShutters , bool middayDown , bool midnightDown ) { auto clamp = [] ( float a , float mn , float mx ) -> float { if (a > mx) { a = mx; } if (a < mn) { a = mn; } return a; }; NodePath sunlightPivotNP = render.find("**/sunlightPivot"); NodePath moonlightPivotNP = render.find("**/moonlightPivot"); NodePath sunlightNP = render.find("**/sunlight"); NodePath moonlightNP = render.find("**/moonlight"); PT(DirectionalLight) sunlight = DCAST(DirectionalLight, sunlightNP.node()); PT(DirectionalLight) moonlight = DCAST(DirectionalLight, moonlightNP.node()); float p = sunlightPivotNP.get_p(); p += speed * delta; if (p > 360) p = 0; if (p < 0) p = 360; if (middayDown) { p = 270; } else if (midnightDown) { p = 90; } sunlightPivotNP.set_p( p ); moonlightPivotNP.set_p(p - 180); float mixFactor = 1.0 - (sin(toRadians(p)) / 2.0 + 0.5); LColor sunlightColor = mixColor(sunlightColor0, sunlightColor1, mixFactor); LColor moonlightColor = mixColor(moonlightColor1, sunlightColor0, mixFactor); LColor lightColor = mixColor(moonlightColor, sunlightColor, mixFactor); float dayTimeLightMagnitude = clamp(-1 * sin(toRadians(p)), 0.0, 1.0); float nightTimeLightMagnitude = clamp( sin(toRadians(p)), 0.0, 1.0); sunlight->set_color( lightColor * dayTimeLightMagnitude); moonlight->set_color(lightColor * nightTimeLightMagnitude); if (dayTimeLightMagnitude > 0.0) { sunlight->set_shadow_caster(true, SHADOW_SIZE, SHADOW_SIZE); render.set_light(sunlightNP); } else { sunlight->set_shadow_caster(false, 0, 0); render.set_light_off(sunlightNP); } if (nightTimeLightMagnitude > 0.0) { moonlight->set_shadow_caster(true, SHADOW_SIZE, SHADOW_SIZE); render.set_light(moonlightNP); } else { moonlight->set_shadow_caster(false, 0, 0); render.set_light_off(moonlightNP); } auto updateWindowLight = [&] ( std::string name ) -> void { NodePath windowLightNP = render.find("**/" + name); PT(Spotlight) windowLight = DCAST(Spotlight, windowLightNP.node()); float windowLightMagnitude = pow(nightTimeLightMagnitude, 0.4); windowLight->set_color(windowLightColor * windowLightMagnitude); if (windowLightMagnitude <= 0.0) { windowLight->set_shadow_caster(false, 0, 0); render.set_light_off(windowLightNP); } else { windowLight->set_shadow_caster(true, SHADOW_SIZE, SHADOW_SIZE); render.set_light(windowLightNP); } }; updateWindowLight("windowLight"); updateWindowLight("windowLight1"); updateWindowLight("windowLight2"); if (mixFactor >= 0.3 && mixFactor <= 0.35 && closedShutters || midnightDown) { closedShutters = false; shuttersAnimationCollection.play("open-shutters"); } else if (mixFactor >= 0.6 && mixFactor <= 0.7 && !closedShutters || middayDown) { closedShutters = true; shuttersAnimationCollection.play("close-shutters"); } return p; } PT(Shader) loadShader ( std::string vert , std::string frag ) { return Shader::load ( Shader::SL_GLSL , "shaders/vertex/" + vert + ".vert" , "shaders/fragment/" + frag + ".frag" ); } PTA_LVecBase3f generateSsaoSamples ( int numberOfSamples ) { auto lerp = [](float a, float b, float f) -> float { return a + f * (b - a); }; PTA_LVecBase3f ssaoSamples = PTA_LVecBase3f(); for (int i = 0; i < numberOfSamples; ++i) { LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) ).normalized(); float rand = randomFloats(generator); sample[0] *= rand; sample[1] *= rand; sample[2] *= rand; float scale = (float) i / (float) numberOfSamples; scale = lerp(0.1, 1.0, scale * scale); sample[0] *= scale; sample[1] *= scale; sample[2] *= scale; ssaoSamples.push_back(sample); } return ssaoSamples; } PTA_LVecBase3f generateSsaoNoise ( int numberOfNoise ) { PTA_LVecBase3f ssaoNoise = PTA_LVecBase3f(); for (int i = 0; i < numberOfNoise; ++i) { LVecBase3f noise = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , 0.0 ); ssaoNoise.push_back(noise); } return ssaoNoise; } FramebufferTexture generateFramebufferTexture ( FramebufferTextureArguments framebufferTextureArguments ) { PT(WindowFramework) window = framebufferTextureArguments.window; PT(GraphicsOutput) graphicsOutput = framebufferTextureArguments.graphicsOutput; PT(GraphicsEngine) graphicsEngine = framebufferTextureArguments.graphicsEngine; LVecBase4 rgbaBits = framebufferTextureArguments.rgbaBits; GraphicsOutput::RenderTexturePlane bitplane = framebufferTextureArguments.bitplane; int aux_rgba = framebufferTextureArguments.aux_rgba; bool setFloatColor = framebufferTextureArguments.setFloatColor; bool setSrgbColor = framebufferTextureArguments.setSrgbColor; bool setRgbColor = framebufferTextureArguments.setRgbColor; bool useScene = framebufferTextureArguments.useScene; std::string name = framebufferTextureArguments.name; LColor clearColor = framebufferTextureArguments.clearColor; FrameBufferProperties fbp = FrameBufferProperties::get_default(); fbp.set_back_buffers(0); fbp.set_rgba_bits ( rgbaBits[0] , rgbaBits[1] , rgbaBits[2] , rgbaBits[3] ); fbp.set_aux_rgba(aux_rgba); fbp.set_float_color(setFloatColor); fbp.set_srgb_color (setSrgbColor ); fbp.set_rgb_color (setRgbColor ); PT(GraphicsOutput) buffer = graphicsEngine ->make_output ( graphicsOutput->get_pipe() , name + "Buffer" , BACKGROUND_RENDER_SORT_ORDER - 1 , fbp , WindowProperties::size(0, 0), GraphicsPipe::BF_refuse_window | GraphicsPipe::BF_resizeable | GraphicsPipe::BF_can_bind_every | GraphicsPipe::BF_rtt_cumulative | GraphicsPipe::BF_size_track_host , graphicsOutput->get_gsg() , graphicsOutput->get_host() ); buffer->add_render_texture ( NULL , GraphicsOutput::RTM_bind_or_copy , bitplane ); buffer->set_clear_color(clearColor); NodePath cameraNP = NodePath(""); PT(Camera) camera = NULL; if (useScene) { cameraNP = window->make_camera(); camera = DCAST(Camera, cameraNP.node()); camera->set_lens(window->get_camera(0)->get_lens()); } else { camera = new Camera(name + "Camera"); PT(OrthographicLens) lens = new OrthographicLens(); lens->set_film_size(2, 2); lens->set_film_offset(0, 0); lens->set_near_far(-1, 1); camera->set_lens(lens); cameraNP = NodePath(camera); } PT(DisplayRegion) bufferRegion = buffer->make_display_region(0, 1, 0, 1); bufferRegion->set_camera(cameraNP); NodePath shaderNP = NodePath(name + "Shader"); if (!useScene) { NodePath renderNP = NodePath(name + "Render"); renderNP.set_depth_test( false); renderNP.set_depth_write(false); cameraNP.reparent_to(renderNP); CardMaker card = CardMaker(name); card.set_frame_fullscreen_quad(); card.set_has_uvs(true); NodePath cardNP = NodePath(card.generate()); cardNP.reparent_to(renderNP); cardNP.set_pos(0, 0, 0); cardNP.set_hpr(0, 0, 0); cameraNP.look_at(cardNP); } FramebufferTexture result; result.buffer = buffer; result.bufferRegion = bufferRegion; result.camera = camera; result.cameraNP = cameraNP; result.shaderNP = shaderNP; return result; } void showBuffer ( NodePath render2d , NodePath statusNP , std::tuple bufferTexture , bool alpha ) { hideBuffer ( render2d ); std::string bufferName; PT(GraphicsOutput) buffer; int texture; std::tie(bufferName, buffer, texture) = bufferTexture; NodePath nodePath = buffer->get_texture_card(); nodePath.set_texture(buffer->get_texture(texture)); nodePath.reparent_to(render2d); nodePath.set_y(0); if (alpha) nodePath.set_transparency ( TransparencyAttrib::Mode::M_alpha ); statusNP.reparent_to(nodePath); } void hideBuffer ( NodePath render2d ) { NodePath nodePath = render2d.find("**/texture card"); if (nodePath) nodePath.detach_node(); } int microsecondsSinceEpoch ( ) { return std::chrono::duration_cast ( std::chrono::system_clock::now().time_since_epoch() ).count(); } bool isButtonDown ( PT(MouseWatcher) mouseWatcher , std::string character ) { return mouseWatcher ->is_button_down ( ButtonRegistry::ptr()->find_button(character) ); } PT(MouseWatcher) getMouseWatcher ( WindowFramework* window ) { return DCAST ( MouseWatcher , window->get_mouse().node() ); } void setSoundOff ( PT(AudioSound) sound ) { setSoundState(sound, false); } void setSoundOn ( PT(AudioSound) sound ) { setSoundState(sound, true); } void setSoundState ( PT(AudioSound) sound , bool on ) { if (!on && sound->status() == AudioSound::PLAYING) { sound->stop(); } else if (on && sound->status() != AudioSound::PLAYING) { sound->play(); } } void updateAudoManager ( NodePath sceneRootNP , NodePath cameraNP ) { LVector3f f = sceneRootNP.get_relative_vector(cameraNP, LVector3f::forward()); LVector3f u = sceneRootNP.get_relative_vector(cameraNP, LVector3f::up()); LVector3f v = LVector3f(0, 0, 0); LVector3f p = cameraNP.get_pos(sceneRootNP); audioManager->audio_3d_set_listener_attributes ( p[0], p[1], p[2] , v[0], v[1], v[2] , f[0], f[1], f[2] , u[0], u[1], u[2] ); audioManager->update(); } LVecBase3f calculateCameraPosition ( double radius , double phi , double theta , LVecBase3 lookAt ) { double x = radius * sin(toRadians(phi)) * cos(toRadians(theta)) + lookAt[0]; double y = radius * sin(toRadians(phi)) * sin(toRadians(theta)) + lookAt[1]; double z = radius * cos(toRadians(phi)) + lookAt[2]; return LVecBase3f(x, y, z); } LVecBase3f calculateCameraLookAt ( double upDownAdjust , double leftRightAdjust , double phi , double theta , LVecBase3 lookAt ) { lookAt[0] += upDownAdjust * sin(toRadians(-theta - 90)) * cos(toRadians(phi)); lookAt[1] += upDownAdjust * cos(toRadians(-theta - 90)) * cos(toRadians(phi)); lookAt[2] -= -upDownAdjust * sin(toRadians(phi)); lookAt[0] += leftRightAdjust * sin(toRadians(-theta)); lookAt[1] += leftRightAdjust * cos(toRadians(-theta)); return lookAt; } NodePath setUpParticles ( NodePath render , PT(Texture) smokeTexture ) { PT(ParticleSystem) smokePS = new ParticleSystem(); PT(ForceNode) smokeFN = new ForceNode("smoke"); PT(PhysicalNode) smokePN = new PhysicalNode("smoke"); smokePS->set_pool_size(75); smokePS->set_birth_rate(0.01); smokePS->set_litter_size(1); smokePS->set_litter_spread(2); smokePS->set_system_lifespan(0.0); smokePS->set_local_velocity_flag(true); smokePS->set_system_grows_older_flag(false); PT(PointParticleFactory) smokePPF = new PointParticleFactory(); smokePPF->set_lifespan_base(0.1); smokePPF->set_lifespan_spread(3); smokePPF->set_mass_base(1); smokePPF->set_mass_spread(0); smokePPF->set_terminal_velocity_base(400); smokePPF->set_terminal_velocity_spread(0); smokePS->set_factory(smokePPF); PT(SpriteParticleRenderer) smokeSPR = new SpriteParticleRenderer(); smokeSPR->set_alpha_mode(BaseParticleRenderer::PR_ALPHA_OUT); smokeSPR->set_user_alpha(1.0); smokeSPR->set_texture(smokeTexture); smokeSPR->set_color(LColor(1.0, 1.0, 1.0, 1.0)); smokeSPR->set_x_scale_flag(true); smokeSPR->set_y_scale_flag(true); smokeSPR->set_anim_angle_flag(true); smokeSPR->set_initial_x_scale(0.0000001); smokeSPR->set_final_x_scale( 0.007); smokeSPR->set_initial_y_scale(0.0000001); smokeSPR->set_final_y_scale( 0.007); smokeSPR->set_nonanimated_theta(209.0546); smokeSPR->set_alpha_blend_method(BaseParticleRenderer::PP_BLEND_CUBIC); smokeSPR->set_alpha_disable(false); smokeSPR->get_color_interpolation_manager()->add_linear ( 0.0 , 1.0 , LColor(1.0, 1.0, 1.0, 1.0) , LColor(0.039, 0.078, 0.156, 1.0) , true ); smokePS->set_renderer(smokeSPR); PT(PointEmitter) smokePE = new PointEmitter(); smokePE->set_emission_type(BaseParticleEmitter::ET_EXPLICIT); smokePE->set_amplitude(0.0); smokePE->set_amplitude_spread(1.0); smokePE->set_offset_force(LVector3f(0.0, 0.0, 2.0)); smokePE->set_explicit_launch_vector(LVector3f(0.0, 0.1, 0.0)); smokePE->set_radiate_origin(LPoint3f(0.0, 0.0, 0.0)); smokePE->set_location(LPoint3f(0.0, 0.0, 0.0)); smokePS->set_emitter(smokePE); PT(LinearVectorForce) smokeLVF = new LinearVectorForce(LVector3f(3.0, -2.0, 0.0), 1.0, false); smokeLVF->set_vector_masks(true, true, true); smokeLVF->set_active(true); smokeFN->add_force(smokeLVF); smokePS->add_linear_force(smokeLVF); PT(LinearJitterForce) smokeLJF = new LinearJitterForce(2.0, false); smokeLJF->set_vector_masks(true, true, true); smokeLJF->set_active(true); smokeFN->add_force(smokeLJF); smokePS->add_linear_force(smokeLJF); PT(LinearCylinderVortexForce) smokeLCVF = new LinearCylinderVortexForce(10.0, 1.0, 4.0, 1.0, false); smokeLCVF->set_vector_masks(true, true, true); smokeLCVF->set_active(true); smokeFN->add_force(smokeLCVF); smokePS->add_linear_force(smokeLCVF); smokePN->insert_physical(0, smokePS); smokePS->set_render_parent(smokePN); NodePath smokeNP = render.attach_new_node(smokePN); smokeNP.attach_new_node(smokeFN); particleSystemManager.attach_particlesystem(smokePS); physicsManager.attach_physical(smokePS); smokeNP.set_pos(0.47, 4.5, 8.9); smokeNP.set_transparency(TransparencyAttrib::M_dual); smokeNP.set_bin("fixed", 0); return smokeNP; } void squashGeometry ( NodePath environmentNP ) { for (int i = 0; i < 4; ++i) { std::string index = std::to_string(i); NodePathCollection treeCollection = environmentNP.find_all_matches("**/tree" + index); NodePath treesNP = NodePath("treeCollection" + index); treesNP.reparent_to(environmentNP); treeCollection.reparent_to(treesNP); treesNP.flatten_strong(); } NodePathCollection barrelCollection = environmentNP.find_all_matches("**/barrel-wood*"); NodePath barrelNP = NodePath("barrels"); barrelNP.reparent_to(environmentNP); barrelCollection.reparent_to(barrelNP); barrelNP.flatten_strong(); NodePath squashNP = NodePath("squash"); squashNP.reparent_to(environmentNP); NodePathCollection squashCollection = environmentNP.find_all_matches("**/*"); for (int i = 0; i < squashCollection.size(); ++i) { if ( squashCollection[i].get_name() == "wheel-lp" || squashCollection[i].get_name() == "water-lp" || squashCollection[i].get_name() == "squash" ) { continue; } squashCollection[i].reparent_to(squashNP); } squashNP.flatten_strong(); } double microsecondToSecond ( int m ) { return m / 1000000.0; } double toRadians ( double d ) { return d * M_PI / 180.0; } LVecBase2f makeEnabledVec ( int t ) { if (t >= 1) { t = 1; } else { t = 0; } return LVecBase2f(t, t); } LVecBase2f toggleEnabledVec ( LVecBase2f vec ) { int t = vec[0]; if (t >= 1) { t = 0; } else { t = 1; } vec[0] = t; vec[1] = t; return vec; } void setTextureToNearestAndClamp ( PT(Texture) texture ) { texture->set_magfilter(SamplerState::FT_nearest); texture->set_minfilter(SamplerState::FT_nearest); texture->set_wrap_u(SamplerState::WM_clamp); texture->set_wrap_v(SamplerState::WM_clamp); texture->set_wrap_w(SamplerState::WM_clamp); } LColor mixColor ( LColor a , LColor b , float factor ) { return a * (1 - factor) + b * factor; } ================================================ FILE: docs/_build-docs.sh ================================================ #!/usr/bin/env bash SCRIPT_PATH="$(cd "$(dirname "$0")"; pwd -P)" MAIN_TITLE="3D Game Shaders For Beginners" REPO_URL="https://github.com/lettier/3d-game-shaders-for-beginners" AUTHOR="David Lettier" CSS="style.css" for f in $SCRIPT_PATH/../sections/* do echo "$f" file=$(basename -- "$f") file_name="${file%.*}" title=$(echo "$file_name" | sed -r 's/-/ /g' | sed -e 's/\b\(.\)/\u\1/g') if [ "$title" == "Ssao" ] then title="SSAO" fi if [ "$title" == "Glsl" ] then title="GLSL" fi $PANDOC \ -f gfm \ -t html5 \ --highlight-style=breezedark \ --template=$SCRIPT_PATH/_template.html5 \ $f \ --metadata pagetitle="$title | $MAIN_TITLE" \ --metadata author-meta="$AUTHOR" \ --metadata css=$CSS \ -o "$SCRIPT_PATH/$file_name.html" done $PANDOC \ -f gfm \ -t html5 \ --highlight-style=breezedark \ --template=$SCRIPT_PATH/_template.html5 \ $SCRIPT_PATH/../README.md \ --metadata pagetitle="$MAIN_TITLE" \ --metadata author-meta="$AUTHOR" \ --metadata css=$CSS \ -o "$SCRIPT_PATH/index.html" for i in {1..3} do for f in $SCRIPT_PATH/* do file=$(basename -- "$f") file_name="${file%.*}" file_ext="${file##*.}" if [ "$file_ext" == "html" ] then echo $f sed -i -E 's/href="(sections\/)?([a-z-]+)\.md(.*)"/href="\2\.html\3"/g' $f sed -i -E 's/href="\.\.\/README.md"/href="index.html"/g' $f sed -i -E 's+++g' $f fi done done ================================================ FILE: docs/_template.html5 ================================================ $for(author-meta)$ $endfor$ $if(date-meta)$ $endif$ $if(keywords)$ $endif$ $if(title-prefix)$$title-prefix$ – $endif$$pagetitle$ $if(highlighting-css)$ $endif$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$ $for(css)$ $endfor$ $for(include-before)$ $include-before$ $endfor$ $if(title)$

$title$

$if(subtitle)$

$subtitle$

$endif$ $for(author)$

$author$

$endfor$ $if(date)$

$date$

$endif$
$endif$ $if(toc)$ $endif$ $body$ $for(include-after)$ $include-after$ $endfor$ ================================================ FILE: docs/blinn-phong.html ================================================ Blinn Phong | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Blinn-Phong

Blinn-Phong

Blinn-Phong is a slight adjustment of the Phong model you saw in the lighting section. It provides more plausible or realistic specular reflections. You'll notice that Blinn-Phong produces elliptical or elongated specular reflections versus the spherical specular reflections produced by the Phong model. In certain cases, Blinn-Phong can be more efficient to calculate than Phong.

  // ...

  vec3 light   = normal(lightPosition.xyz - vertexPosition.xyz);
  vec3 eye     = normalize(-vertexPosition.xyz);
  vec3 halfway = normalize(light + eye);

  // ...

Instead of computing the reflection vector, compute the halfway or half angle vector. This vector is between the view/camera/eye and light direction vector.

Blinn-Phong vs Phong

    // ...

    float specularIntensity = dot(normal, halfway);

    // ...

The specular intensity is now the dot product of the normal and halfway vector. In the Phong model, it is the dot product of the reflection and view vector.

Full specular intensity.

The half angle vector (magenta arrow) will point in the same direction as the normal (green arrow) when the view vector (orange arrow) points in the same direction as the reflection vector (magenta arrow). In this case, both the Blinn-Phong and Phong specular intensity will be one.

Blinn-Phong vs Phong

In other cases, the specular intensity for Blinn-Phong will be greater than zero while the specular intensity for Phong will be zero.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/bloom.html ================================================ Bloom | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Bloom

Bloom

Adding bloom to a scene can really sell the illusion of the lighting model. Light emitting objects are more believable and specular highlights get an extra dose of shimmer.

  //...

  int   size       = 5;
  float separation = 3;
  float threshold  = 0.4;
  float amount     = 1;

  // ...

These parameters control the look and feel. size determines how blurred the effect is. separation spreads out the blur. threshold controls which fragments are illuminated. And the last parameter, amount, controls how much bloom is outputted.

  // ...

  vec2 texSize = textureSize(colorTexture, 0).xy;

  float value = 0.0;
  float count = 0.0;

  vec4 result = vec4(0);
  vec4 color  = vec4(0);

  for (int i = -size; i <= size; ++i) {
    for (int j = -size; j <= size; ++j) {
      // ...
    }
  }

  // ...

The technique starts by looping through a kernel/matrix/window centered over the current fragment. This is similar to the window used for outlining. The size of the window is size * 2 + 1 by size * 2 + 1. So for example, with a size setting of two, the window uses (2 * 2 + 1)^2 = 25 samples per fragment.

      // ...

      color =
        texture
          ( colorTexture
          ,   ( gl_FragCoord.xy
              + (vec2(i, j) * separation)
              )
            / texSize
          );

      value = max(color.r, max(color.g, color.b));
      if (value < threshold) { color = vec4(0); }

      result += color;
      count  += 1.0;

      // ...

For each iteration, it retrieves the color from the input texture and turns the red, green, and blue values into a greyscale value. If this greyscale value is less than the threshold, it discards this color by making it solid black. After evaluating the sample's greyscale value, it adds its RGB values to result.

  // ...

  result /= count;

  fragColor = mix(vec4(0), result, amount);

  // ...

After it's done summing up the samples, it divides the sum of the color samples by the number of samples taken. The result is the average color of itself and its neighbors. By doing this for every fragment, you end up with a blurred image. This form of blurring is known as a box blur.

Bloom progresssion.

Here you see the progression of the bloom algorithm.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/blur.html ================================================ Blur | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Blur

Kuwahara Filter

The need to blur this or that can come up quite often as you try to obtain a particular look or perform some technique like motion blur. Below are just some of ways you can blur your game's imagery.

Box Blur

Box Blur

The box blur or mean filter algorithm is a simple to implement blurring effect. It's fast and gets the job done. If you need more finesse, you can upgrade to a Gaussian blur.

  // ...

  vec2 texSize  = textureSize(colorTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  int size  = int(parameters.x);
  if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; }

  // ...

The size parameter controls how blurry the result is. If the size is zero or less, return the fragment untouched.

  // ...

  float separation = parameters.y;
        separation = max(separation, 1);

  // ...

The separation parameter spreads out the blur without having to sample additional fragments. separation ranges from one to infinity.

Blur Kernel

  // ...

  for (int i = -size; i <= size; ++i) {
    for (int j = -size; j <= size; ++j) {
      // ...
    }
  }

  // ...

Like the outlining technique, the box blur technique uses a kernel/matrix/window centered around the current fragment. The size of the window is size * 2 + 1 by size * 2 + 1. So for example, with a size setting of two, the window uses (2 * 2 + 1)^2 = 25 samples per fragment.

      // ...

      fragColor +=
        texture
          ( colorTexture
          ,   ( gl_FragCoord.xy
              + (vec2(i, j) * separation)
              )
            / texSize
          );

      // ...

To compute the mean or average of the samples in the window, start by loop through the window, adding up each color vector.

  // ...

  fragColor /= pow(size * 2 + 1, 2);

  // ...

To finish computing the mean, divide the sum of the colors sampled by the number of samples taken. The final fragment color is the mean or average of the fragments sampled inside the window.

Median Filter

Median Filter

The box blur uses the mean color of the samples taken. The median filter uses the median color of the samples taken. By using the median instead of the mean, the edges in the image are preserved—meaning the edges stay nice and crisp. For example, look at the windows in the box blurred image versus the median filtered image.

Unfortunately, finding the median can be slower than finding the mean. You could sort the values and choose the middle one but that would take at least quasilinear time. There is a technique to find the median in linear time but it can be quite awkward inside a shader. The numerical approach below approximates the median in linear time. How well it approximates the median can be controlled.

Painterly

At lower quality approximations, you end up with a nice painterly look.

// ...

#define MAX_SIZE        4
#define MAX_KERNEL_SIZE ((MAX_SIZE * 2 + 1) * (MAX_SIZE * 2 + 1))
#define MAX_BINS_SIZE   100

// ...

These are the hard limits for the size parameter, window size, and bins array.

  // ...

  vec2 texSize  = textureSize(colorTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  int size = int(parameters.x);
  if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; }
  if (size > MAX_SIZE) { size = MAX_SIZE; }
  int kernelSize = int(pow(size * 2 + 1, 2));

  // ...

The size parameter controls how blurry or smeared the effect is. If the size is at or below zero, return the current fragment untouched. From the size parameter, calculate the total size of the kernel or window. This is how many samples you'll be taking per fragment.

  // ...

  int binsSize = int(parameters.y);
      binsSize = clamp(binsSize, 1, MAX_BINS_SIZE);

  // ...

Set up the binsSize, making sure to limit it by the MAX_BINS_SIZE.

  // ...

  int i        = 0;
  int j        = 0;
  int count    = 0;
  int binIndex = 0;

  // ...

i and j are used to sample the given texture around the current fragment. i is also used as a general for loop count. count is used in the initialization of the colors array which you'll see later. binIndex is used to approximate the median color.

  // ...

  vec4  colors[MAX_KERNEL_SIZE];
  float bins[MAX_BINS_SIZE];
  int   binIndexes[colors.length()];

  // ...

The colors array holds the sampled colors taken from the input texture. bins is used to approximate the median of the sampled colors. Each bin holds a count of how many colors fall into its range when converting each color into a greyscale value (between zero and one). As binsSize approaches 100, the algorithm finds the true median almost always. binIndexes stores the bins index or which bin each sample falls into.

  // ...

  float total = 0;
  float limit = floor(float(kernelSize) / 2) + 1;

  // ...

total keeps track of how many colors you've come across as you loop through bins. When total reaches limit, you return whatever bins index you're at. The limit is the median index. For example, if the window size is 81, limit is 41 which is directly in the middle (40 samples below and 40 samples above).

  // ...

  float value       = 0;
  vec3  valueRatios = vec3(0.3, 0.59, 0.11);

  // ...

These are used to covert and hold each color sample's greyscale value. Instead of dividing red, green, and blue by one third, it uses 30% of red, 59% of green, and 11% of blue for a total of 100%.

  // ...

  for (i = -size; i <= size; ++i) {
    for (j = -size; j <= size; ++j) {
      colors[count] =
        texture
          ( colorTexture
          ,   ( gl_FragCoord.xy
              + vec2(i, j)
              )
            / texSize
          );
      count += 1;
    }
  }

  // ...

Loop through the window and collect the color samples into colors.

  // ...

  for (i = 0; i < binsSize; ++i) {
    bins[i] = 0;
  }

  // ...

Initialize the bins array with zeros.

  // ...

  for (i = 0; i < kernelSize; ++i) {
    value           = dot(colors[i].rgb, valueRatios);
    binIndex        = int(floor(value * binsSize));
    binIndex        = clamp(binIndex, 0, binsSize - 1);
    bins[binIndex] += 1;
    binIndexes[i]   = binIndex;
  }

  // ...

Loop through the colors and convert each one to a greyscale value. dot(colors[i].rgb, valueRatios) is the weighted sum colors.r * 0.3 + colors.g * 0.59 + colors.b * 0.11.

Each value will fall into some bin. Each bin covers some range of values. For example, if the number of bins is 10, the first bin covers everything from zero up to but not including 0.1. Increment the number of colors that fall into this bin and remember the color sample's bin index so you can look it up later.

  // ...

  binIndex = 0;

  for (i = 0; i < binsSize; ++i) {
    total += bins[i];
    if (total >= limit) {
      binIndex = i;
      break;
    }
  }

  // ...

Loop through the bins, tallying up the number of colors seen so far. When you reach the median index, exit the loop and remember the last bins index reached.

  // ...

  fragColor = colors[0];

  for (i = 0; i < kernelSize; ++i) {
    if (binIndexes[i] == binIndex) {
      fragColor = colors[i];
      break;
    }
  }

  // ...

Now loop through the binIndexes and find the first color with the last bins indexed reached. Its greyscale value is the approximated median which in many cases will be the true median value. Set this color as the fragColor and exit the loop and shader.

Kuwahara Filter

Kuwahara Filter

Like the median filter, the kuwahara filter preserves the major edges found in the image. You'll notice that it has a more block like or chunky pattern to it. In practice, the Kuwahara filter runs faster than the median filter, allowing for larger size values without a noticeable slowdown.

// ...

#define MAX_SIZE        5
#define MAX_KERNEL_SIZE ((MAX_SIZE * 2 + 1) * (MAX_SIZE * 2 + 1))

// ...

Set a hard limit for the size parameter and the number of samples taken.

// ...

int i     = 0;
int j     = 0;
int count = 0;

// ...

These are used to sample the input texture and set up the values array.

// ...

vec3  valueRatios = vec3(0.3, 0.59, 0.11);

// ...

Like the median filter, you'll be converting the color samples into greyscale values.

// ...

float values[MAX_KERNEL_SIZE];

// ...

Initialize the values array. This will hold the greyscale values for the color samples.

// ...

vec4  color       = vec4(0);
vec4  meanTemp    = vec4(0);
vec4  mean        = vec4(0);
float valueMean   = 0;
float variance    = 0;
float minVariance = -1;

// ...

The Kuwahara filter works by computing the variance of four subwindows and then using the mean of the subwindow with the smallest variance.

// ...

void findMean(int i0, int i1, int j0, int j1) {

// ...

findMean is a function defined outside of main. Each run of findMean will remember the mean of the given subwindow that has the lowest variance seen so far.

  // ...

  meanTemp = vec4(0);
  count    = 0;

  // ...

Make sure to reset count and meanTemp before computing the mean of the given subwindow.

  // ...

  for (i = i0; i <= i1; ++i) {
    for (j = j0; j <= j1; ++j) {
      color  =
        texture
          ( colorTexture
          ,   (gl_FragCoord.xy + vec2(i, j))
            / texSize
          );

      meanTemp += color;

      values[count] = dot(color.rgb, valueRatios);

      count += 1;
    }
  }

  // ...

Similar to the box blur, loop through the given subwindow and add up each color. At the same time, make sure to store the greyscale value for this sample in values.

  // ...

  meanTemp.rgb /= count;
  valueMean     = dot(meanTemp.rgb, valueRatios);

  // ...

To compute the mean, divide the samples sum by the number of samples taken. Calculate the greyscale value for the mean.

  // ...

  for (i = 0; i < count; ++i) {
    variance += pow(values[i] - valueMean, 2);
  }

  variance /= count;

  // ...

Now calculate the variance for this given subwindow. The variance is the average squared difference between each sample's greyscale value the mean greyscale value.

  // ...

  if (variance < minVariance || minVariance <= -1) {
    mean = meanTemp;
    minVariance = variance;
  }
}

// ...

If the variance is smaller than what you've seen before or this is the first variance you've seen, set the mean of this subwindow as the final mean and update the minimum variance seen so far.

// ...

void main() {
  int size = int(parameters.x);
  if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; }

  // ...

Back in main, set the size parameter. If the size is at or below zero, return the fragment unchanged.

Kuwahara Kernal

  // ...

  // Lower Left

  findMean(-size, 0, -size, 0);

  // Upper Right

  findMean(0, size, 0, size);

  // Upper Left

  findMean(-size, 0, 0, size);

  // Lower Right

  findMean(0, size, -size, 0);

  // ...

As stated above, the Kuwahara filter works by computing the variance of four subwindows and then using the mean of the subwindow with the lowest variance as the final fragment color. Note that the four subwindows overlap each other.

  // ...

  mean.a    = 1;
  fragColor = mean;

  // ...

After computing the variance and mean for each subwindow, set the fragment color to the mean of the subwindow with the lowest variance.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/building-the-demo.html ================================================ Building The Demo | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Building The Demo

Building The Demo

Before you can try out the demo program, you'll have to build the example code first.

Dependencies

Before you can compile the example code, you'll need to install Panda3D for your platform. Panda3D is available for Linux, Mac, and Windows.

Linux

Start by installing the Panda3D SDK for your distribution.

Make sure to locate where the Panda3D headers and libraries are. The headers and libraries are most likely in /usr/include/panda3d/ and /usr/lib/panda3d/ respectively.

Next clone this repository and change directory into it.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners/demonstration

Now compile the source code into an object file.

g++ \
  -c src/main.cxx \
  -o 3d-game-shaders-for-beginners.o \
  -std=gnu++11 \
  -O2 \
  -I/path/to/python/include/ \
  -I/path/to/panda3d/include/

With the object file created, create the executable by linking the object file to its dependencies.

g++ \
  3d-game-shaders-for-beginners.o \
  -o 3d-game-shaders-for-beginners \
  -L/path/to/panda3d/lib \
  -lp3framework \
  -lpanda \
  -lpandafx \
  -lpandaexpress \
  -lpandaphysics \
  -lp3dtoolconfig \
  -lp3dtool \
  -lpthread

For more help, see the Panda3D manual.

Mac

Start by installing the Panda3D SDK for Mac.

Make sure to locate where the Panda3D headers and libraries are.

Next clone this repository and change directory into it.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners

Now compile the source code into an object file. You'll have to find where the Python 2.7 and Panda3D include directories are.

clang++ \
  -c main.cxx \
  -o 3d-game-shaders-for-beginners.o \
  -std=gnu++11 \
  -g \
  -O2 \
  -I/path/to/python/include/ \
  -I/path/to/panda3d/include/

With the object file created, create the executable by linking the object file to its dependencies. You'll need to track down where the Panda3D libraries are located.

clang++ \
  3d-game-shaders-for-beginners.o \
  -o 3d-game-shaders-for-beginners \
  -L/path/to/panda3d/lib \
  -lp3framework \
  -lpanda \
  -lpandafx \
  -lpandaexpress \
  -lpandaphysics \
  -lp3dtoolconfig \
  -lp3dtool \
  -lpthread

For more help, see the Panda3D manual.

Windows

Start by installing the Panda3D SDK for Windows.

Make sure to locate where the Panda3D headers and libraries are.

Next clone this repository and change directory into it.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners

For more help, see the Panda3D manual.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/cel-shading.html ================================================ Cel Shading | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Cel Shading

Cel Shaded

Cel shading is a technique to make 3D objects look 2D or flat. In 2D, you can make an object look 3D by applying a smooth gradient. However, with cel shading, you're breaking up the gradients into abrupt transitions. Typically there is only one transition where the shading goes from fully lit to fully shadowed. When combined with outlining, cel shading can really sell the 2D cartoon look.

Diffuse

    // ...

    float diffuseIntensity = max(dot(normal, unitLightDirection), 0.0);
          diffuseIntensity = step(0.1, diffuseIntensity);

    // ...

Revisiting the lighting model, modify the diffuseIntensity such that it is either zero or one.

Step Function

The step function returns zero if the input is less than the edge and one otherwise.

Steps Function

  // ...

  if      (diffuseIntensity >= 0.8) { diffuseIntensity = 1.0; }
  else if (diffuseIntensity >= 0.6) { diffuseIntensity = 0.6; }
  else if (diffuseIntensity >= 0.3) { diffuseIntensity = 0.3; }
  else                              { diffuseIntensity = 0.0; }

  // ...

If you would like to have a few steps or transitions, you can perform something like the above.

Step Texture

  // ...

  diffuseIntensity = texture(steps, vec2(diffuseIntensity, 0.0)).r;

  // ...

Another approach is to put your step values into a texture with the transitions going from darker to lighter. Using the diffuseIntensity as a U coordinate, it will automatically transform itself.

Specular


      // ...

      float specularIntensity = clamp(dot(normal, halfwayDirection), 0.0, 1.0);
            specularIntensity = step(0.98, specularIntensity);

      // ...

Using the step function again, set the specularIntensity to be either zero or one. You can also use one of the other approaches described up above for the specular highlight as well. After you've altered the specularIntensity, the rest of the lighting calculations are the same.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/chromatic-aberration.html ================================================ Chromatic Aberration | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Chromatic Aberration

Chromatic Aberration

Chromatic aberration is a screen space technique that simulates lens distortion. Use it to give your scene a cinematic, lo-fi analog feel or to emphasize a chaotic event.

Texture

uniform sampler2D colorTexture;

// ...

The input texture needed is the scene's colors captured into a framebuffer texture.

Parameters

Chromatic Aberration

  // ...

  float redOffset   =  0.009;
  float greenOffset =  0.006;
  float blueOffset  = -0.006;

  // ...

The adjustable parameters for this technique are the red, green, and blue offsets. Feel free to play around with these to get the particular color fringe you're looking for. These particular offsets produce a yellowish orange and blue fringe.

Direction

// ...

uniform vec2 mouseFocusPoint;

  // ...

  vec2 texSize  = textureSize(colorTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec2 direction = texCoord - mouseFocusPoint;

  // ...

The offsets can occur horizontally, vertically, or radially. One approach is to radiate out from the depth of field focal point. As the scene gets more out of focus, the chromatic aberration increases.

Samples

// ...

out vec4 fragColor;

  // ...

  fragColor.r  = texture(colorTexture, texCoord + (direction * vec2(redOffset  ))).r;
  fragColor.g  = texture(colorTexture, texCoord + (direction * vec2(greenOffset))).g;
  fragColor.ba = texture(colorTexture, texCoord + (direction * vec2(blueOffset ))).ba;
}

With the direction and offsets, make three samples of the scene's colors—one for the red, green, and blue channels. These will be the final fragment color.

Source

(C) 2021 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/deferred-rendering.html ================================================ Deferred Rendering | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Deferred Rendering

Deferred vs Forward

Deferred rendering (deferred shading) is a screen space lighting technique. Instead of calculating the lighting for a scene while you traverse its geometry—you defer or wait to perform the lighting calculations until after the scene's geometry fragments have been culled or discarded. This can give you a performance boost depending on the complexity of your scene.

Phases

G-buffer

Deferred rendering is performed in two phases. The first phase involves going through the scene's geometry and rendering its positions or depths, normals, and materials into a framebuffer known as the geometry buffer or G-buffer. With the exception of some transformations, this is mostly a read-only phase so its performance cost is minimal. After this phase, you're only dealing with 2D textures in the shape of the screen.

Lighting Phase

The second and last phase is where you perform your lighting calculations using the output of the first phase. This is when you calculate the ambient, diffuse, and specular colors. Shadow and normal mapping are performed in this phase as well.

Advantages

The reason for using deferred rendering is to reduce the number of lighting calculations made. With forward rendering, the number of lighting calculations scales with the number of fragments and lights. However, with deferred shading, the number of lighting calculations scales with the number of pixels and lights. Recall that for a single pixel, there can be multiple fragments produced. As you add geometry, the number of lighting calculations per pixel increases when using forward but not when using deferred.

For simple scenes, deferred rendering doesn't provide much of a performance boost and may even hurt performance. However, for complex scenes with lots of lighting, it becomes the better option. Deferred becomes faster than forward because you're only calculating the lighting per light, per pixel. In forward rendering, you're calculating the lighting per light per fragment which can be multiple times per pixel.

Disadvantages

Deferred rendering allows you render complex scenes using many lights but it does come with its own set of tradeoffs. Transparency becomes an issue because the geometry data you could see through a semitransparent object is discarded in the first phase. Other tradesoffs include increased memory consumption due to the G-buffer and the extra workarounds needed to deal with aliasing.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/depth-of-field.html ================================================ Depth Of Field | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Depth Of Field

Depth Of Field

Like SSAO, depth of field is an effect you can't live without once you've used it. Artistically, you can use it to draw your viewer's eye to a certain subject. But in general, depth of field adds a lot of realism for a little bit of effort.

In Focus

The first step is to render your scene completely in focus. Be sure to render this into a framebuffer texture. This will be one of the inputs to the depth of field shader.

Out Of Focus

The second step is to blur the scene as if it was completely out of focus. Like bloom and SSAO, you can use a box blur. Be sure to render this out-of-focus-scene into a framebuffer texture. This will be one of the inputs to the depth of field shader.

Bokeh

Bokeh

For a great bokeh effect, dilate the out of focus texture and use that as the out of focus input. See dilation for more details.

Mixing

  // ...

  float minDistance = 1.0;
  float maxDistance = 3.0;

  // ...

Feel free to tweak these two parameters. All positions at or below minDistance will be completely in focus. All positions at or beyond maxDistance will be completely out of focus.

  // ...

  vec4 focusColor      = texture(focusTexture, texCoord);
  vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord);

  // ...

You'll need the in focus and out of focus colors.

  // ...

  vec4 position = texture(positionTexture, texCoord);

  // ...

You'll also need the vertex position in view space. You can reuse the position framebuffer texture you used for SSAO.

  // ...

  vec4 focusPoint = texture(positionTexture, mouseFocusPoint);

  // ...

The focus point is a position somewhere in your scene. All of the points in your scene are measured from the focus point.

Choosing the focus point is up to you. The demo uses the scene position directly under the mouse when clicking the middle mouse button. However, it could be a constant distance from the camera or a static position.

smoothstep

  // ...

  float blur =
    smoothstep
      ( minDistance
      , maxDistance
      , abs(position.y - focusPoint.y)
      );

  // ...

smoothstep returns values from zero to one. The minDistance is the left-most edge. Any position less than the minimum distance, from the focus point, will be in focus or have a blur of zero. The maxDistance is the right-most edge. Any position greater than the maximum distance, from the focus point, will be out of focus or have a blur or one. For distances between the edges, blur will be between zero and one. These values are interpolated along a s-shaped curve.


  // ...

  fragColor = mix(focusColor, outOfFocusColor, blur);

  // ...

The fragColor is a mixture of the in focus and out of focus color. The closer blur is to one, the more it will use the outOfFocusColor. Zero blur means this fragment is entirely in focus. At blur >= 1, this fragment is completely out of focus.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/dilation.html ================================================ Dilation | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Dilation

Dilation

Dilation dilates or enlarges the brighter areas of an image while at the same time, contracts or shrinks the darker areas of an image. This tends to create a pillowy look. You can use dilation for a glow/bloom effect or to add bokeh to your depth of field.

  // ...

  int   size         = int(parameters.x);
  float separation   =     parameters.y;
  float minThreshold = 0.1;
  float maxThreshold = 0.3;

  // ...

The size and separation parameters control how dilated the image becomes. A larger size will increase the dilation at the cost of performance. A larger separation will increase the dilation at the cost of quality. The minThreshold and maxThreshold parameters control which parts of the image become dilated.

  // ...

  vec2 texSize   = textureSize(colorTexture, 0).xy;
  vec2 fragCoord = gl_FragCoord.xy;

  fragColor = texture(colorTexture, fragCoord / texSize);

  // ...

Sample the color at the current fragment's position.

  // ...

  float  mx = 0.0;
  vec4  cmx = fragColor;

  for (int i = -size; i <= size; ++i) {
    for (int j = -size; j <= size; ++j) {
      // ...
    }
  }

  // ...

Loop through a size by size window, centered at the current fragment position. As you loop, find the brightest color based on the surrounding greyscale values.

Dilation Window

      // ...

      // For a rectangular shape.
      //if (false);

      // For a diamond shape;
      //if (!(abs(i) <= size - abs(j))) { continue; }

      // For a circular shape.
      if (!(distance(vec2(i, j), vec2(0, 0)) <= size)) { continue; }

      // ...

The window shape will determine the shape of the dilated parts of the image. For a rectangular shape, you can use every fragment covered by the window. For any other shape, skip the fragments that fall outside the desired shape.

      // ...

      vec4 c =
        texture
          ( colorTexture
          ,   ( gl_FragCoord.xy
              + (vec2(i, j) * separation)
              )
            / texSize
          );

      // ...

Sample a fragment color from the surrounding window.

      // ...

      float mxt = dot(c.rgb, vec3(0.21, 0.72, 0.07));

      // ...

Convert the sampled color to a greyscale value.

      // ...

      if (mxt > mx) {
        mx  = mxt;
        cmx = c;
      }

      // ...

If the sampled greyscale value is larger than the current maximum greyscale value, update the maximum greyscale value and its corresponding color.

  // ...

  fragColor.rgb =
    mix
      ( fragColor.rgb
      , cmx.rgb
      , smoothstep(minThreshold, maxThreshold, mx)
      );

  // ...

The new fragment color is a mixture between the existing fragment color and the brightest color found. If the maximum greyscale value found is less than minThreshold, the fragment color is unchanged. If the maximum greyscale value is greater than maxThreshold, the fragment color is replaced with the brightest color found. For any other case, the fragment color is a mix between the current fragment color and the brightest color.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/film-grain.html ================================================ Film Grain | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Film Grain

Film Grain

Film grain (when applied in subtle doses, unlike here) can add a bit of realism you don't notice until it's removed. Typically, it's the imperfections that make a digitally generated image more believable. In terms of the shader graph, film grain is usually the last effect applied before the game is put on screen.

Amount

  // ...

  float amount = 0.1;

  // ...

The amount controls how noticeable the film grain is. Crank it up for a snowy picture.

Random Intensity

// ...

uniform float osg_FrameTime;

  //...

  float toRadians = 3.14 / 180;

    //...

    float randomIntensity =
      fract
        ( 10000
        * sin
            (
              ( gl_FragCoord.x
              + gl_FragCoord.y
              * osg_FrameTime
              )
            * toRadians
            )
        );

    // ...

This snippet calculates the random intensity needed to adjust the amount.

Time Since F1 = 00 01 02 03 04 05 06 07 08 09 10
Frame Number  = F1    F3    F4       F5 F6
osg_FrameTime = 00    02    04       07 08

osg_FrameTime is provided by Panda3D. The frame time is a timestamp of how many seconds have passed since the first frame. The example code uses this to animate the film grain as osg_FrameTime will always be different each frame.


              // ...

              ( gl_FragCoord.x
              + gl_FragCoord.y
              * 8009 // Large number here.

              // ...

For static film grain, replace osg_FrameTime with a large number. You may have to try different numbers to avoid seeing any patterns.

Horizontal, vertical, and diagonal lines.

        // ...

        * sin
            (
              ( gl_FragCoord.x
              + gl_FragCoord.y
              * someNumber

              // ...

Both the x and y coordinate are used to create points or specs of film grain. If only x was used, there would only be vertical lines. Similarly, if only y was used, there would be only horizontal lines.

The reason the snippet multiplies one coordinate by some number is to break up the diagonal symmetry.

Rain

You can of course remove the coordinate multiplier for a somewhat decent looking rain effect. To animate the rain effect, multiply the output of sin by osg_FrameTime.

              // ...

              ( gl_FragCoord.x
              + gl_FragCoord.y

              // ...

Play around with the x and y coordinate to try and get the rain to change directions. Keep only the x coordinate for a straight downpour.

input = (gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime) * toRadians
  frame(10000 * sin(input)) =
    fract(10000 * sin(6.977777777777778)) =
      fract(10000 * 0.6400723818964882) =

sin is used as a hashing function. The fragment's coordinates are hashed to some output of sin. This has the nice property that no matter the input (big or small), the output range is negative one to one.

fract(10000 * sin(6.977777777777778)) =
  fract(10000 * 0.6400723818964882) =
    fract(6400.723818964882) =
      0.723818964882

sin is also used as a pseudo random number generator when combined with fract.

>>> [floor(fract(4     * sin(x * toRadians)) * 10) for x in range(0, 10)]
[0, 0, 1, 2, 2, 3, 4, 4, 5, 6]

>>> [floor(fract(10000 * sin(x * toRadians)) * 10) for x in range(0, 10)]
[0, 4, 8, 0, 2, 1, 7, 0, 0, 5]

Take a look at the first sequence of numbers and then the second. Each sequence is deterministic but the second sequence has less of a pattern than the first. So while the output of fract(10000 * sin(...)) is deterministic, it doesn't have much of a discernible pattern.

Increasing the pseudo randomness.

Here you see the sin multiplier going from 1, to 10, to 100, and then to 1000.

As you increase the sin output multiplier, you get less and less of a pattern. This is the reason the snippet multiplies sin by 10,000.

Fragment Color

  // ...

  vec2 texSize  = textureSize(colorTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 color = texture(colorTexture, texCoord);

  // ...

Convert the fragment's coordinates to UV coordinates. Using these UV coordinates, look up the texture color for this fragment.

    // ...

    amount *= randomIntensity;

    color.rgb += amount;

    // ...

Adjust the amount by the random intensity and add this to the color.

  // ...

  fragColor = color;

  // ...

Set the fragment color and you're done.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/flow-mapping.html ================================================ Flow Mapping | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Flow Mapping

Flow Mapping

Flow mapping is useful when you need to animate some fluid material. Much like diffuse maps map UV coordinates to diffuse colors and normal maps map UV coordinates to normals, flow maps map UV coordinates to 2D translations or flows.

Flow Map

Here you see a flow map that maps UV coordinates to translations in the positive y-axis direction. Flow maps use the red and green channels to store translations in the x and y direction. The red channel is for the x-axis and the green channel is the y-axis. Both range from zero to one which translates to flows that range from (-1, -1) to (1, 1). This flow map is all one color consisting of 0.5 red and 0.6 green.

[r, g, b] =
  [r * 2 - 1, g * 2 - 1, b * 2 - 1] =
    [ x, y, z]

Recall how the colors in a normal map are converted to actual normals. There is a similar process for flow maps.

// ...

uniform sampler2D flowTexture;

  vec2 flow = texture(flowTexture, uv).xy;
       flow = (flow - 0.5) * 2;

  // ...

To convert a flow map color to a flow, you minus 0.5 from the channel (red and green) and multiply by two.

(r, g) =
 ( (r - 0.5) * 2
 , (g - 0.5) * 2
 ) =
  ( (0.5 - 0.5) * 2
  , (0.6 - 0.5) * 2
  ) =
    (x, y) =
      (0, 0.2)

The flow map above maps each UV coordinate to the flow (0, 0.2). This indicates zero movement in the x direction and a movement of 0.2 in the y direction.

The flows can be used to translate all sorts of things but they're typically used to offset the UV coordinates of a another texture.

Foam Mask

  // ...

  vec2 flow = texture(flowTexture, diffuseCoord).xy;
       flow = (flow - 0.5) * 2;

  vec4 foamPattern =
    texture
      ( foamPatternTexture
      , vec2
          ( diffuseCoord.x + flow.x * osg_FrameTime
          , diffuseCoord.y + flow.y * osg_FrameTime
          )
      );

  // ...

For example, the demo program uses a flow map to animate the water. Here you see the flow map being used to animate the foam mask. This continuously moves the diffuse UV coordinates directly up, giving the foam mask the appearance of moving down stream.

          // ...

          ( diffuseCoord.x + flow.x * osg_FrameTime
          , diffuseCoord.y + flow.y * osg_FrameTime

          // ...

You'll need how many seconds have passed since the first frame in order to animate the UV coordinates in the direction indicated by the flow. osg_FrameTime is provided by Panda3D. It is a timestamp of how many seconds have passed since the first frame.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/foam.html ================================================ Foam | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Foam

Foam

Foam is typically used when simulating some body of water. Anywhere the water's flow is disrupted, you add some foam. The foam isn't much by itself but it can really connect the water with the rest of the scene.

Lava River

But don't stop at just water. You can use the same technique to make a river of lava for example.

Vertex Positions

Like screen space refraction, you'll need both the foreground and background vertex positions. The foreground being the scene with the foamy surface and the background being the scene without the foamy surface. Referrer back to SSAO for the details on how to acquire the vertex positions in view space.

Mask

Foam Mask

You'll need to texture your scene with a foam mask. The demo masks everything off except the water. For the water, it textures it with a foam pattern.

// ...

uniform sampler2D foamPatternTexture;

in vec2 diffuseCoord;

out vec4 fragColor;

void main() {
  vec4 foamPattern = texture(foamPatternTexture, diffuseCoord);

  fragColor = vec4(vec3(dot(foamPattern.rgb, vec3(1)) / 3), 1);
}

Here you see the fragment shader that generates the foam mask. It takes a foam pattern texture and UV maps it to the scene's geometry using the diffuse UV coordinates. For every model, except the water, the shader is given a solid black texture as the foamPatternTexture.

  // ...

  fragColor = vec4(vec3(dot(foamPattern.rgb, vec3(1)) / 3), 1);

  // ...

The fragment color is converted to greyscale, as a precaution, since the foam shader expects the foam mask to be greyscale.

Uniforms

// ...

uniform sampler2D maskTexture;
uniform sampler2D positionFromTexture;
uniform sampler2D positionToTexture;

// ...

The foam shader accepts a mask texture, the foreground vertex positions (positionFromTexture), and the background vertex positions (positionToTexture).

Parameters

  // ...

  float foamDepth = 4;
  vec4  foamColor = vec4(0.8, 0.85, 0.92, 1);

  // ...

The adjustable parameters for the foam shader are the foam depth and color. The foam depth controls how much foam is shown. As the foam depth increases, the amount of foam shown increases.

Distance

  // ...

  vec4 positionFrom = texture(positionFromTexture, texCoord);
  vec4 positionTo   = texture(positionToTexture,   texCoord);

  float depth  = (positionTo.xyz - positionFrom.xyz).y;

  // ...

Compute the distance from the foreground position to the background position. Since the positions are in view (camera) space, we only need the y value since it goes into the screen.

Amount

  // ...

  float amount  = clamp(depth / foamDepth.x, 0, 1);
        amount  = 1 - amount;
        amount *= mask.r;
        amount  = amount * amount / (2 * (amount * amount - amount) + 1);

  // ...

The amount of foam is based on the depth, the foam depth parameter, and the mask value.

Easing equation.

        // ...

        amount  = amount * amount / (2 * (amount * amount - amount) + 1);

        // ...

Reshape the amount using the ease in and out easing function. This will give a lot of foam near depth zero and little to no foam near foamDepth.

Fragment Color

  // ...

  fragColor = mix(vec4(0), foamColor, amount);

  // ...

The fragment color is a mix between transparent black and the foam color based on the amount.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/fog.html ================================================ Fog | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Fog

Fog

Fog (or mist as it's called in Blender) adds atmospheric haze to a scene, providing mystique and softening pop-ins (geometry suddenly entering into the camera's frustum).

// ...

uniform vec4 color;

uniform vec2 nearFar;

// ...

To calculate the fog, you'll need its color, near distance, and far distance.

// ...

uniform sampler2D positionTexture;

  // ...

  vec2 texSize  = textureSize(positionTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 position = texture(positionTexture, texCoord);

  // ...

In addition to the fog's attributes, you'll also need the fragment's vertex position.

  float fogMin = 0.00;
  float fogMax = 0.97;

fogMax controls how much of the scene is still visible when the fog is most intense. fogMin controls how much of the fog is still visible when the fog is least intense.

  // ...

  float near = nearFar.x;
  float far  = nearFar.y;

  float intensity =
    clamp
      (   (position.y - near)
        / (far        - near)
      , fogMin
      , fogMax
      );

  // ...

The example code uses a linear model for calculating the fog's intensity. There's also an exponential model you could use.

The fog's intensity is fogMin before or at the start of the fog's near distance. As the vertex position gets closer to the end of the fog's far distance, the intensity moves closer to fogMax. For any vertexes after the end of the fog, the intensity is clamped to fogMax.

  // ...

  fragColor = vec4(color.rgb, intensity);

  // ...

Set the fragment's color to the fog color and the fragment's alpha channel to the intensity. As intensity gets closer to one, you'll have less of the scene's color and more of the fog color. When intensity reaches one, you'll have all fog color and nothing else.

// ...

uniform sampler2D baseTexture;
uniform sampler2D fogTexture;

  // ...

  vec2 texSize  = textureSize(baseTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 baseColor = texture(baseTexture, texCoord);
  vec4 fogColor  = texture(fogTexture,  texCoord);

  fragColor = baseColor;

  // ...

  fragColor = mix(fragColor, fogColor, min(fogColor.a, 1));

  // ...

Normally you calculate the fog in the same shader that does the lighting calculations. However, you can also break it out into its own framebuffer texture. Here you see the fog color being applied to the rest of the scene much like you would apply a layer in GIMP. This allows you to calculate the fog once instead calculating it in every shader that eventually needs it.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/fresnel-factor.html ================================================ Fresnel Factor | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Fresnel Factor

Fresnel

The fresnel factor alters the reflectiveness of a surface based on the camera or viewing angle. As a surface points away from the camera, its reflectiveness goes up. Similarly, as a surface points towards the camera, its reflectiveness goes down.

In other words, as a surface becomes perpendicular with the camera, it becomes more mirror like. Utilizing this property, you can vary the opacity of reflections (such as specular and screen space reflections) and/or vary a surface's alpha values for a more plausible or realistic look.

Specular Reflection

Specular Intensity

  vec4 specular =   materialSpecularColor
                  * lightSpecularColor
                  * pow(max(dot(eye, reflection), 0.0), shininess);

In the lighting section, the specular component was a combination of the material's specular color, the light's specular color, and by how much the camera pointed into the light's reflection direction. Incorporating the fresnel factor, you'll now vary the material specular color based on the angle between the camera and the surface it's pointed at.

  // ...

  vec3 eye = normalize(-vertexPosition.xyz);

  // ...

The first vector you'll need is the eye/view/camera vector. Recall that the eye vector points from the vertex position to the camera's position. If the vertex position is in view or camera space, the eye vector is the vertex position pointed in the opposite direction.

  // ...

  vec3 light   = normal(lightPosition.xyz - vertexPosition.xyz);
  vec3 halfway = normalize(light + eye);

  // ...

The fresnel factor is calculated using two vectors. The simplest two vectors to use are the eye and normal vector. However, if you're using the halfway vector (from the Blinn-Phong section), you can instead calculate the fresnel factor using the halfway and eye vector.


  // ...

  float fresnelFactor = dot(halfway, eye); // Or dot(normal, eye).
        fresnelFactor = max(fresnelFactor, 0.0);
        fresnelFactor = 1.0 - fresnelFactor;
        fresnelFactor = pow(fresnelFactor, fresnelPower);

  // ...

With the needed vectors in hand, you can now compute the fresnel factor. The fresnel factor ranges from zero to one. When the dot product is one, the fresnel factor is zero. When the dot product is less than or equal to zero, the fresnel factor is one. This equation comes from Schlick's approximation.

Fresnel Power

In Schlick's approximation, the fresnelPower is five but you can alter this to your liking. The demo code varies it using the blue channel of the specular map with a maximum value of five.

  // ...

  materialSpecularColor.rgb = mix(materialSpecularColor.rgb, vec3(1.0), fresnelFactor);

  // ...

Once the fresnel factor is determined, use it to modulate the material's specular color. As the fresnel factor approaches one, the material becomes more like a mirror or fully reflective.

  // ...

  vec4 specular      = vec4(vec3(0.0), 1.0);
       specular.rgb  =   materialSpecularColor.rgb
                       * lightSpecularColor.rgb
                       * pow
                          ( max(dot(normal, halfway), 0.0) // Or max(dot(reflection, eye), 0.0).
                          , shininess
                          );

  // ...

As before, the specular component is a combination of the material's specular color, the light's specular color, and by how much the camera points into the direction of the light's reflection. However, using the fresnel factor, the material's specular color various depending on the orientation of the camera and the surface it's looking at.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/gamma-correction.html ================================================ Gamma Correction | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Gamma Correction

Gamma Correction

Correcting for gamma will make your color calculations look correct. This isn't to say they'll look amazing but with gamma correction, you'll find that the colors meld together better, the shadows are more nuanced, and the highlights are more subtle. Without gamma correction, the shadowed areas tend to get crushed while the highlighted areas tend to get blown-out and over saturated making for a harsh contrast overall.

If you're aiming for realism, gamma correction is especially important. As you perform more and more calculations, the tiny errors add up making it harder to achieve photorealism. The equations will be correct but the inputs and outputs will be wrong leaving you frustrated.

It's easy to get twisted around when thinking about gamma correction but essentially it boils down to knowing what color space a color is in and how to convert that color to the color space you need. With those two pieces of the puzzle, gamma correction becomes a tedious yet simple chore you'll have to perform from time to time.

Color Spaces

sRGB vs RGB

The two color spaces you'll need to be aware of are sRGB (standard Red Green Blue) and RGB or linear color space.

identify -format "%[colorspace]\n" house-diffuse-srgb.png
sRGB

identify -format "%[colorspace]\n" house-diffuse-rgb.png
RGB

Knowing what color space a color texture is in will determine how you handle it in your shaders. To determine the color space of a texture, use ImageMagick's identify. You'll find that most textures are in sRGB.

convert house-diffuse-srgb -colorspace rgb house-diffuse-rgb.png

To convert a texture to a particular color space, use ImageMagick's convert program. Notice how a texture is darkened when transforming from sRGB to RGB.

Decoding

The red, green, and blue values in a sRGB color texture are encoded and cannot be modified directly. Modifying them directly would be like running spellcheck on an encrypted message. Before you can run spellcheck, you first have to decrypt the message. Similarly, to modify the values of an sRGB texture, you first have to decode or transform them to RGB or linear color space.

  // ...

  color     = texture(color_texture, uv);
  color.rgb = pow(color.rgb, 2.2);

  // ...

To decode a sRGB encoded color, raise the rgb values to the power of 2.2. Once you have decoded the color, you are now free to add, subtract, multiply, and divide it.

By raising the color values to the power of 2.2, you're converting them from sRGB to RGB or linear color space. This conversion has the effect of darkening the colors.

Color Darkening

For example, vec3(0.9, 0.2, 0.3) becomes vec3(0.793, 0.028, 0.07).

Gamma Curves

The 2.2 value is known as gamma. Loosely speaking, gamma can either be 1.0 / 2.2, 2.2, or 1.0. As you've seen, 2.2 is for decoding sRGB encoded color textures. As you will see, 1.0 / 2.2 is for encoding linear or RGB color textures. And 1.0 is RGB or linear color space since y = 1 * x + 0 and any base raised to the power of 1.0 is itself.

Non-color Data

Non-color Data

One important exception to decoding is when the "colors" of a texture represent non-color data. Some examples of non-color data would be the normals in a normal map, the alpha channel, the heights in a height map, and the directions in a flow map. Only decode color related data or data that represents color. When dealing with non-color data, treat the sRGB color values as RGB or linear and skip the decoding process.

Encoding

Perceptually versus Actually Linear

The necessity for encoding and decoding stems from the fact that humans do not perceive lightness linearly and most displays (like a monitor) lack the precision or number of bits to accurately show both lighter and darker tonal values or shades. With only so many bits to go around, colors are encoded in such a way that more bits are devoted to the darker shades than the lighter shades since humans are more sensitive to darker tones than lighter tones. Encoding it this way uses the limited number of bits more effectively for human perception. Still, the only thing to remember is that your display is expecting sRGB encoded values. Therefore, if you decoded a sRGB value, you have to encode it before it makes its way to your display.

  // ...

  color     = texture(color_texture, uv);
  color.rgb = pow(color.rgb, 1.0 / 2.2);

  // ...

To encode a linear value or convert RGB to sRGB, raise the rgb values to the power of 1.0 / 2.2. Notice how 1.1 / 2.2 is the reciprocal of 2.2 or 2.2 / 1.0. Here you see the symmetry in decoding and encoding.

Not Gamma Corrected versus Gamma Corrected

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/glsl.html ================================================ GLSL | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

GLSL

Instead of using the fixed-function pipeline, you'll be using the programmable GPU rendering pipeline. Since it is programmable, it is up to you to supply the programming in the form of shaders. A shader is a (typically small) program you write using a syntax reminiscent of C. The programmable GPU rendering pipeline has various different stages that you can program with shaders. The different types of shaders include vertex, tessellation, geometry, fragment, and compute. You'll only need to focus on the vertex and fragment stages for the techniques below.

#version 150

void main() {}

Here is a bare-bones GLSL shader consisting of the GLSL version number and the main function.

#version 150

uniform mat4 p3d_ModelViewProjectionMatrix;

in vec4 p3d_Vertex;

void main()
{
  gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}

Here is a stripped down GLSL vertex shader that transforms an incoming vertex to clip space and outputs this new position as the vertex's homogeneous position. The main procedure doesn't return anything since it is void and the gl_Position variable is a built-in output.

Take note of the keywords uniform and in. The uniform keyword means this global variable is the same for all vertexes. Panda3D sets the p3d_ModelViewProjectionMatrix for you and it is the same matrix for each vertex. The in keyword means this global variable is being given to the shader. The vertex shader receives each vertex that makes up the geometry the vertex shader is attached to.

#version 150

out vec4 fragColor;

void main() {
  fragColor = vec4(0, 1, 0, 1);
}

Here is a stripped down GLSL fragment shader that outputs the fragment color as solid green. Keep in mind that a fragment affects at most one screen pixel but a single pixel can be affected by many fragments.

Take note of the out keyword. The out keyword means this global variable is being set by the shader. The name fragColor is arbitrary so feel free to choose a different one.

Output of the stripped down shaders.

This is the output of the two shaders shown above.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/index.html ================================================ 3D Game Shaders For Beginners

3D Game Shaders For Beginners

3D Game Shaders For Beginners

Interested in adding textures, lighting, shadows, normal maps, glowing objects, ambient occlusion, reflections, refractions, and more to your 3D game? Great! Below is a collection of shading techniques that will take your game visuals to new heights. I've explained each technique in such a way that you can take what you learn here and apply/port it to whatever stack you use—be it Godot, Unity, Unreal, or something else. For the glue in between the shaders, I've chosen the fabulous Panda3D game engine and the OpenGL Shading Language (GLSL). So if that is your stack, then you'll also get the benefit of learning how to use these shading techniques with Panda3D and OpenGL specifically.

Table Of Contents

License

The included license applies only to the software portion of 3D Game Shaders For Beginners— specifically the .cxx, .vert, and .frag source code files. No other portion of 3D Game Shaders For Beginners has been licensed for use.

Attributions

(C) 2019 David Lettier
lettier.com

================================================ FILE: docs/lighting.html ================================================ Lighting | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Lighting

Lighting

Completing the lighting involves calculating and combining the ambient, diffuse, specular, and emission light aspects. The example code uses either Phong or Blinn-Phong lighting.

Vertex

// ...

uniform struct p3d_LightSourceParameters
  { vec4 color

  ; vec4 ambient
  ; vec4 diffuse
  ; vec4 specular

  ; vec4 position

  ; vec3  spotDirection
  ; float spotExponent
  ; float spotCutoff
  ; float spotCosCutoff

  ; float constantAttenuation
  ; float linearAttenuation
  ; float quadraticAttenuation

  ; vec3 attenuation

  ; sampler2DShadow shadowMap

  ; mat4 shadowViewMatrix
  ;
  } p3d_LightSource[NUMBER_OF_LIGHTS];

// ...

For every light, minus the ambient light, Panda3D gives you this convenient struct which is available to both the vertex and fragment shaders. The biggest convenience being the shadow map and shadow view matrix for transforming vertexes to shadow or light space.

  // ...

  vertexPosition = p3d_ModelViewMatrix * p3d_Vertex;

  // ...

  for (int i = 0; i < p3d_LightSource.length(); ++i) {
    vertexInShadowSpaces[i] = p3d_LightSource[i].shadowViewMatrix * vertexPosition;
  }

  // ...

Starting in the vertex shader, you'll need to transform and output the vertex from view space to shadow or light space for each light in your scene. You'll need this later in the fragment shader in order to render the shadows. Shadow or light space is where every coordinate is relative to the light position (the light is the origin).

Fragment

The fragment shader is where most of the lighting calculations take place.

Material

// ...

uniform struct
  { vec4 ambient
  ; vec4 diffuse
  ; vec4 emission
  ; vec3 specular
  ; float shininess
  ;
  } p3d_Material;

// ...

Panda3D gives us the material (in the form of a struct) for the mesh or model you are currently rendering.

Multiple Lights

  // ...

  vec4 diffuse  = vec4(0.0, 0.0, 0.0, diffuseTex.a);
  vec4 specular = vec4(0.0, 0.0, 0.0, diffuseTex.a);

  // ...

Before you loop through the scene's lights, create an accumulator for both the diffuse and specular colors.

  // ...

  for (int i = 0; i < p3d_LightSource.length(); ++i) {
    // ...
  }

  // ...

Now you can loop through the lights, calculating the diffuse and specular colors for each one.

Phong Lighting Model

Here you see the four major vectors you'll need to calculate the diffuse and specular colors contributed by each light. The light direction vector is the light blue arrow pointing to the light. The normal vector is the green arrow standing straight up. The reflection vector is the dark blue arrow mirroring the light direction vector. The eye or view vector is the orange arrow pointing towards the camera.

    // ...

    vec3 lightDirection =
        p3d_LightSource[i].position.xyz
      - vertexPosition.xyz
      * p3d_LightSource[i].position.w;

    // ...

The light direction is from the vertex's position to the light's position.

Panda3D sets p3d_LightSource[i].position.w to zero if this is a directional light. Directional lights do not have a position as they only have a direction. So if this is a directional light, the light direction will be the negative or opposite direction of the light as Panda3D sets p3d_LightSource[i].position.xyz to be -direction for directional lights.

  // ...

  normal = normalize(vertexNormal);

  // ...

You'll need the vertex normal to be a unit vector. Unit vectors have a length of magnitude of one.

    // ...

    vec3 unitLightDirection = normalize(lightDirection);
    vec3 eyeDirection       = normalize(-vertexPosition.xyz);
    vec3 reflectedDirection = normalize(-reflect(unitLightDirection, normal));

    // ...

Next you'll need three more vectors.

You'll need to take the dot product involving the light direction so its best to normalize it. This gives it a distance or magnitude of one (unit vector).

The eye direction is the opposite of the vertex/fragment position since the vertex/fragment position is relative to the camera's position. Remember that the vertex/fragment position is in view space. So instead of going from the camera (eye) to the vertex/fragment, you go from the vertex/fragment to the eye (camera).

The reflection vector is a reflection of the light direction at the surface normal. As the light "ray" hits the surface, it bounces off at the same angle it came in at. The angle between the light direction vector and the normal is known as the "angle of incidence". The angle between the reflection vector and the normal is known as the "angle of reflection".

You'll have to negate the reflected light vector as it needs to point in the same direction as the eye vector. Remember the eye direction is from the vertex/fragment to the camera position. You'll use the reflection vector to calculate the intensity of the specular highlight.

Diffuse

    // ...

    float diffuseIntensity  = dot(normal, unitLightDirection);

    if (diffuseIntensity < 0.0) { continue; }

    // ...

The diffuse intensity is the dot product between the surface normal and the unit vector light direction. The dot product can range from negative one to one. If both vectors point in the same direction, the intensity is one. Any other case will be less than one.

The light direction versus the normal direction.

As the light vector approaches the same direction as the normal, the diffuse intensity approaches one.

    // ...

    if (diffuseIntensity < 0.0) { continue; }

    // ...

If the diffuse intensity is zero or less, move on to the next light.

    // ...

    vec4 diffuseTemp =
      vec4
        ( clamp
            (   diffuseTex.rgb
              * p3d_LightSource[i].diffuse.rgb
              * diffuseIntensity
            , 0
            , 1
            )
        , diffuseTex.a
        );

    diffuseTemp = clamp(diffuseTemp, vec4(0), diffuseTex);

    // ...

You can now calculate the diffuse color contributed by this light. If the diffuse intensity is one, the diffuse color will be a mix between the diffuse texture color and the lights color. Any other intensity will cause the diffuse color to be darker.

Notice how I clamp the diffuse color to be only as bright as the diffuse texture color is. This will protect the scene from being over exposed. When creating your diffuse textures, make sure to create them as if they were fully lit.

Specular

After diffuse, comes specular.

Specular Intensity

    // ...

    float specularIntensity = max(dot(reflectedDirection, eyeDirection), 0);

    vec4 specularTemp =
      clamp
        (   vec4(p3d_Material.specular, 1)
          * p3d_LightSource[i].specular
          * pow
              ( specularIntensity
              , p3d_Material.shininess
              )
        , 0
        , 1
        );

    // ...

The specular intensity is the dot product between the eye vector and the reflection vector. As with the diffuse intensity, if the two vectors point in the same direction, the specular intensity is one. Any other intensity will diminish the amount of specular color contributed by this light.

Shininess

The material shininess determines how spread out the specular highlight is. This is typically set in a modeling program like Blender. In Blender it's known as the specular hardness.

Spotlights

    // ...

    float unitLightDirectionDelta =
      dot
        ( normalize(p3d_LightSource[i].spotDirection)
        , -unitLightDirection
        );

    if (unitLightDirectionDelta < p3d_LightSource[i].spotCosCutoff) { continue; }

    // ...
}

This snippet keeps fragments outside of a spotlight's cone or frustum from being affected by the light. Fortunately, Panda3D sets up spotDirection and spotCosCutoff to also work for directional lights and points lights. Spotlights have both a position and direction. However, directional lights only have a direction and point lights only have a position. Still, this code works for all three lights avoiding the need for noisy if statements.

        // ...

        , -unitLightDirection

        // ...

You must negate unitLightDirection. unitLightDirection goes from the fragment to the spotlight and you need it to go from the spotlight to the fragment since the spotDirection goes directly down the center of the spotlight's frustum some distance away from the spotlight's position.

spotCosCutoff = cosine(0.5 * spotlightLensFovAngle);

For a spotlight, if the dot product between the fragment-to-light vector and the spotlight's direction vector is less than the cosine of half the spotlight's field of view angle, the shader disregards this light's influence.

For directional lights and point lights, Panda3D sets spotCosCutoff to negative one. Recall that the dot product ranges from negative one to one. So it doesn't matter what the unitLightDirectionDelta is because it will always be greater than or equal to negative one.

    // ...

    diffuseTemp *= pow(unitLightDirectionDelta, p3d_LightSource[i].spotExponent);

    // ...

Like the unitLightDirectionDelta snippet, this snippet also works for all three light types. For spotlights, this will make the fragments brighter as you move closer to the center of the spotlight's frustum. For directional lights and point lights, spotExponent is zero. Recall that anything to the power of zero is one so the diffuse color is one times itself meaning it is unchanged.

Shadows

    // ...

    float shadow =
      textureProj
        ( p3d_LightSource[i].shadowMap
        , vertexInShadowSpaces[i]
        );

    diffuseTemp.rgb  *= shadow;
    specularTemp.rgb *= shadow;

    // ...

Panda3D makes applying shadows relatively easy by providing the shadow map and shadow transformation matrix for every scene light. To create the shadow transformation matrix yourself, you'll need to assemble a matrix that transforms view space coordinates to light space (coordinates are relative to the light's position). To create the shadow map yourself, you'll need to render the scene from the perspective of the light to a framebuffer texture. The framebuffer texture must hold the distances from the light to the fragments. This is known as a "depth map". Lastly, you'll need to manually give to your shader your DIY depth map as a uniform sampler2DShadow and your DIY shadow transformation matrix as a uniform mat4. At this point, you've recreated what Panda3D does for you automatically.

The shadow snippet shown uses textureProj which is different from the texure function shown earlier. textureProj first divides vertexInShadowSpaces[i].xyz by vertexInShadowSpaces[i].w. After this, it uses vertexInShadowSpaces[i].xy to locate the depth stored in the shadow map. Next it uses vertexInShadowSpaces[i].z to compare this vertex's depth against the shadow map depth at vertexInShadowSpaces[i].xy. If the comparison passes, textureProj will return one. Otherwise, it will return zero. Zero meaning this vertex/fragment is in the shadow and one meaning this vertex/fragment is not in the shadow.

textureProj can also return a value between zero and one depending on how the shadow map was set up. In this instance, textureProj performs multiple depth tests using neighboring depth values and returns a weighted average. This weighted average can give shadows a softer look.

Attenuation

Attenuation

    // ...

    float lightDistance = length(lightDirection);

    float attenuation =
        1
      / ( p3d_LightSource[i].constantAttenuation
        + p3d_LightSource[i].linearAttenuation
        * lightDistance
        + p3d_LightSource[i].quadraticAttenuation
        * (lightDistance * lightDistance)
        );

    diffuseTemp.rgb  *= attenuation;
    specularTemp.rgb *= attenuation;

    // ...

The light's distance is just the magnitude or length of the light direction vector. Notice it's not using the normalized light direction as that distance would be one.

You'll need the light distance to calculate the attenuation. Attenuation meaning the light's influence diminishes as you get further away from it.

You can set constantAttenuation, linearAttenuation, and quadraticAttenuation to whatever values you would like. A good starting point is constantAttenuation = 1, linearAttenuation = 0, and quadraticAttenuation = 1. With these settings, the attenuation is one at the light's position and approaches zero as you move further away.

Final Light Color

    // ...

    diffuse  += diffuseTemp;
    specular += specularTemp;

    // ...

To calculate the final light color, add the diffuse and specular together. Be sure to add this to the accumulator as you loop through the scene's lights.

Ambient

// ...

uniform sampler2D p3d_Texture1;

// ...

uniform struct
  { vec4 ambient
  ;
  } p3d_LightModel;

// ...

in vec2 diffuseCoord;

  // ...

  vec4 diffuseTex  = texture(p3d_Texture1, diffuseCoord);

  // ...

  vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex;

// ...

The ambient component to the lighting model is based on the material's ambient color, the ambient light's color, and the diffuse texture color.

There should only ever be one ambient light. Because of this, the ambient color calculation only needs to occur once. Contrast this with the diffuse and specular color which must be accumulated for each spot/directional/point light. When you reach SSAO, you'll revisit the ambient color calculation.

Putting It All Together

  // ...

  vec4 outputColor = ambient + diffuse + specular + p3d_Material.emission;

  // ...

The final color is the sum of the ambient color, diffuse color, specular color, and the emission color.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/lookup-table.html ================================================ Lookup Table | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Lookup Table (LUT)

LUT

The lookup table or LUT shader allows you to transform the colors of your game using an image editor like the GIMP. From color grading to turning day into night, the LUT shader is a handy tool for tweaking the look of your game.

Neutral LUT

Before you can get started, you'll need to find a neutral LUT image. Neutral meaning that it leaves the fragment colors unchanged. The LUT needs to be 256 pixels wide by 16 pixels tall and contain 16 blocks with each block being 16 by 16 pixels.

The LUT is mapped out into 16 blocks. Each block has a different level of blue. As you move across the blocks, from left to right, the amount of blue increases. You can see the amount of blue in each block's upper-left corner. Within each block, the amount of red increases as you move from left to right and the amount of green increases as you move from top to bottom. The upper-left corner of the first block is black since every RGB channel is zero. The lower-right corner of the last block is white since every RGB channel is one.

LUT And Screenshot

With the neutral LUT in hand, take a screenshot of your game and open it in your image editor. Add the neutral LUT as a new layer and merge it with the screenshot. As you manipulate the colors of the screenshot, the LUT will be altered in the same way. When you're done editing, select only the LUT and save it as a new image. You now have your new lookup table and can begin writing your shader.

  // ...

  vec2 texSize = textureSize(colorTexture, 0).xy;

  vec4 color = texture(colorTexture, gl_FragCoord.xy / texSize);

  // ...

The LUT shader is a screen space technique. Therefore, sample the scene's color at the current fragment or screen position.

  // ...

  float u  =  floor(color.b * 15.0) / 15.0 * 240.0;
        u  = (floor(color.r * 15.0) / 15.0 *  15.0) + u;
        u /= 255.0;

  float v  = ceil(color.g * 15.0);
        v /= 15.0;
        v  = 1.0 - v;

  // ...

In order to transform the current fragment's color, using the LUT, you'll need to map the color to two UV coordinates on the lookup table texture. The first mapping (shown up above) is to the nearest left or lower bound block location and the second mapping (shown below) is to the nearest right or upper bound block mapping. At the end, you'll combine these two mappings to create the final color transformation.

RGB Channel Mapping

Each of the red, green, and blue channels maps to one of 16 possibilities in the LUT. The blue channel maps to one of the 16 upper-left block corners. After the blue channel maps to a block, the red channel maps to one of the 16 horizontal pixel positions within the block and the green channel maps to one of the 16 vertical pixel positions within the block. These three mappings will determine the UV coordinate you'll need to sample a color from the LUT.

  // ...

        u /= 255.0;

        v /= 15.0;
        v  = 1.0 - v;

  // ...

To calculate the final U coordinate, divide it by 255 since the LUT is 256 pixels wide and U ranges from zero to one. To calculate the final V coordinate, divide it by 15 since the LUT is 16 pixels tall and V ranges from zero to one. You'll also need to subtract the normalized V coordinate from one since V ranges from zero at the bottom to one at the top while the green channel ranges from zero at the top to 15 at the bottom.

  // ...

  vec3 left = texture(lookupTableTexture, vec2(u, v)).rgb;

  // ...

Using the UV coordinates, sample a color from the lookup table. This is the nearest left block color.

  // ...

  u  =  ceil(color.b * 15.0) / 15.0 * 240.0;
  u  = (ceil(color.r * 15.0) / 15.0 *  15.0) + u;
  u /= 255.0;

  v  = 1.0 - (ceil(color.g * 15.0) / 15.0);

  vec3 right = texture(lookupTableTexture, vec2(u, v)).rgb;

  // ...

Now you'll need to calculate the UV coordinates for the nearest right block color. Notice how ceil or ceiling is being used now instead of floor.

Mixing

  // ...

  color.r = mix(left.r, right.r, fract(color.r * 15.0));
  color.g = mix(left.g, right.g, fract(color.g * 15.0));
  color.b = mix(left.b, right.b, fract(color.b * 15.0));

  // ...

Not every channel will map perfectly to one of its 16 possibilities. For example, 0.5 doesn't map perfectly. At the lower bound (floor), it maps to 0.4666666666666667 and at the upper bound (ceil), it maps to 0.5333333333333333. Compare that with 0.4 which maps to 0.4 at the lower bound and 0.4 at the upper bound. For those channels which do not map perfectly, you'll need to mix the left and right sides based on where the channel falls between its lower and upper bound. For 0.5, it falls directly between them making the final color a mixture of half left and half right. However, for 0.132 the mixture will be 98% right and 2% left since the fractional part of 0.123 times 15.0 is 0.98.

  // ...

  fragColor = color;

  // ...

Set the fragment color to the final mix and you're done.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/motion-blur.html ================================================ Motion Blur | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Motion Blur

Motion Blur

To really sell the illusion of speed, you can do no better than motion blur. From high speed car chases to moving at warp speed, motion blur greatly improves the look and feel of fast moving objects.

There are a few ways to implement motion blur as a screen space technique. The less involved implementation will only blur the scene in relation to the camera's movements while the more involved version will blur any moving objects even with the camera remaining still. The less involved technique is described below but the principle is the same.

Textures

uniform sampler2D positionTexture;
uniform sampler2D colorTexture;

// ...

The input textures needed are the vertex positions in view space and the scene's colors. Refer back to SSAO for acquiring the vertex positions.

Matrices

// ...

uniform mat4 previousViewWorldMat;
uniform mat4 worldViewMat;
uniform mat4 lensProjection;

// ...

The motion blur technique determines the blur direction by comparing the previous frame's vertex positions with the current frame's vertex positions. To do this, you'll need the previous frame's view-to-world matrix, the current frame's world-to-view matrix, and the camera lens' projection matrix.

Parameters

// ...

uniform vec2 parameters;

// ...

void main() {
  int   size       = int(parameters.x);
  float separation =     parameters.y;

// ...

The adjustable parameters are size and separation. size controls how many samples are taken along the blur direction. Increasing size increases the amount of blur at the cost of performance. separation controls how spread out the samples are along the blur direction. Increasing separation increases the amount of blur at the cost of accuracy.

Blur Direction

  // ...

  vec2 texSize  = textureSize(colorTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 position1 = texture(positionTexture, texCoord);
  vec4 position0 = worldViewMat * previousViewWorldMat * position1;

  // ...

To determine which way to blur this fragment, you'll need to know where things were last frame and where things are this frame. To figure out where things are now, sample the current vertex position. To figure out where things were last frame, transform the current position from view space to world space, using the previous frame's view-to-world matrix, and then transform it back to view space from world space using this frame's world-to-view matrix. This transformed position is this fragment's previous interpolated vertex position.

Position Projection

  // ...

  position0      = lensProjection * position0;
  position0.xyz /= position0.w;
  position0.xy   = position0.xy * 0.5 + 0.5;

  position1      = lensProjection * position1;
  position1.xyz /= position1.w;
  position1.xy   = position1.xy * 0.5 + 0.5;

  // ...

Now that you have the current and previous positions, transform them to screen space. With the positions in screen space, you can trace out the 2D direction you'll need to blur the onscreen image.

  // ...

  //   position1.xy = position0.xy + direction;
  vec2 direction    = position1.xy - position0.xy;

  if (length(direction) <= 0.0) { return; }

  // ...

The blur direction goes from the previous position to the current position.

Blurring

  // ...

  fragColor = texture(colorTexture, texCoord);

  // ...

Sample the current fragment's color. This will be the first of the colors blurred together.

  // ...

  direction.xy *= separation;

  // ...

Multiply the direction vector by the separation.

  // ...

  vec2  forward  = texCoord;
  vec2  backward = texCoord;

  // ...

For a more seamless blur, sample in the direction of the blur and in the opposite direction of the blur. For now, set the two vectors to the fragment's UV coordinate.

  // ...

  float count = 1.0;

  // ...

count is used to average all of the samples taken. It starts at one since you've already sampled the current fragment's color.

  // ...

  for (int i = 0; i < size; ++i) {
    forward  += direction;
    backward -= direction;

    fragColor +=
      texture
        ( colorTexture
        , forward
        );
    fragColor +=
      texture
        ( colorTexture
        , backward
        );

    count += 2.0;
  }

  // ...

Sample the screen's colors both in the forward and backward direction of the blur. Be sure to add these samples together as you travel along.

  // ...

  fragColor /= count;
}

The final fragment color is the average color of the samples taken.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/normal-mapping.html ================================================ Normal Mapping | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Normal Mapping

Normal Mapping

Normal mapping allows you to add surface details without adding any geometry. Typically, in a modeling program like Blender, you create a high poly and a low poly version of your mesh. You take the vertex normals from the high poly mesh and bake them into a texture. This texture is the normal map. Then inside the fragment shader, you replace the low poly mesh's vertex normals with the high poly mesh's normals you baked into the normal map. Now when you light your mesh, it will appear to have more polygons than it really has. This will keep your FPS high while at the same time retain most of the details from the high poly version.

From high to low poly with normal mapping.

Here you see the progression from the high poly model to the low poly model to the low poly model with the normal map applied.

Normal Map Illusion

Keep in mind though, normal mapping is only an illusion. After a certain angle, the surface will look flat again.

Vertex

// ...

uniform mat3 p3d_NormalMatrix;

// ...

in vec3 p3d_Normal;

// ...

in vec3 p3d_Binormal;
in vec3 p3d_Tangent;

  // ...

  vertexNormal = normalize(p3d_NormalMatrix * p3d_Normal);
  binormal     = normalize(p3d_NormalMatrix * p3d_Binormal);
  tangent      = normalize(p3d_NormalMatrix * p3d_Tangent);

  // ...

Starting in the vertex shader, you'll need to output to the fragment shader the normal vector, binormal vector, and the tangent vector. These vectors are used, in the fragment shader, to transform the normal map normal from tangent space to view space.

p3d_NormalMatrix transforms the vertex normal, binormal, and tangent vectors to view space. Remember that in view space, all of the coordinates are relative to the camera's position.

[p3d_NormalMatrix] is the upper 3x3 of the inverse transpose of the ModelViewMatrix. It is used to transform the normal vector into view-space coordinates.

// ...

in vec2 p3d_MultiTexCoord0;

// ...

out vec2 normalCoord;

  // ...

  normalCoord   = p3d_MultiTexCoord0;

  // ...

Normal Maps

You'll also need to output, to the fragment shader, the UV coordinates for the normal map.

Fragment

Recall that the vertex normal was used to calculate the lighting. However, the normal map provides us with different normals to use when calculating the lighting. In the fragment shader, you need to swap out the vertex normals for the normals found in the normal map.

// ...

uniform sampler2D p3d_Texture1;

// ...

in vec2 normalCoord;

  // ...

  /* Find */
  vec4 normalTex   = texture(p3d_Texture1, normalCoord);

  // ...

Using the normal map coordinates the vertex shader sent, pull out the normal from the normal map.

  // ...

  vec3 normal;

    // ...

    /* Unpack */
    normal =
      normalize
        ( normalTex.rgb
        * 2.0
        - 1.0
        );

    // ...

Earlier I showed how the normals are mapped to colors to create the normal map. Now this process needs to be reversed so you can get back the original normals that were baked into the map.

(r, g, b) =
  ( r * 2 - 1
  , g * 2 - 1
  , b * 2 - 1
  ) =
    (x, y, z)

Here's the process for unpacking the normals from the normal map.

    // ...

    /* Transform */
    normal =
      normalize
        ( mat3
            ( tangent
            , binormal
            , vertexNormal
            )
        * normal
        );

    // ...

The normals you get back from the normal map are typically in tangent space. They could be in another space, however. For example, Blender allows you to bake the normals in tangent, object, world, or camera space.

Replacing the vertex normals with the normal map normals.

To take the normal map normal from tangent space to view pace, construct a three by three matrix using the tangent, binormal, and vertex normal vectors. Multiply the normal by this matrix and be sure to normalize it.

At this point, you're done. The rest of the lighting calculations are the same.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/outlining.html ================================================ Outlining | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Outlining

Outlined Scene

Outlining your scene's geometry can give your game a distinctive look, reminiscent of comic books and cartoons.

Discontinuities

The process of outlining is the process of finding and labeling discontinuities or differences. Every time you find what you consider a significant difference, you mark it with your line color. As you go about labeling or coloring in the differences, outlines or edges will start to form.

Where you choose to search for the discontinuities is up to you. It could be the diffuse colors in your scene, the normals of your models, the depth buffer, or some other scene related data.

The demo uses the interpolated vertex positions to render the outlines. However, a less straightforward but more typical way is to use both the scene's normals and depth buffer values to construct the outlines.

Vertex Positions

// ...

uniform sampler2D positionTexture;

// ...

Like SSAO, you'll need the vertex positions in view space. Referrer back to SSAO for details.

Scene Colors

// ...

uniform sampler2D colorTexture;

// ...

The demo darkens the colors of the scene where there's an outline. This tends to look nicer than a constant color since it provides some color variation to the edges.

Parameters

  // ...

  float minSeparation = 1.0;
  float maxSeparation = 3.0;
  float minDistance   = 0.5;
  float maxDistance   = 2.0;
  int   size          = 1;
  vec3 colorModifier  = vec3(0.324, 0.063, 0.099);

  // ...

The min and max separation parameters control the thickness of the outline depending on the fragment's distance from the camera or depth. The min and max distance control the significance of any changes found. The size parameter controls the constant thickness of the line no matter the fragment's position. The outline color is based on colorModifier and the current fragment's color.

Fragment Position

  // ...

  vec2 texSize   = textureSize(colorTexture, 0).xy;
  vec2 fragCoord = gl_FragCoord.xy;
  vec2 texCoord  = fragCoord / texSize;

  vec4 position = texture(positionTexture, texCoord);

  // ...

Sample the position texture for the current fragment's position in the scene. Recall that the position texture is just a screen shaped quad making the UV coordinate the current fragment's screen coordinate divided by the dimensions of the screen.

Fragment Depth

  // ...

  float depth =
    clamp
      (   1.0
        - ( (far - position.y)
          / (far - near)
        )
      , 0.0
      , 1.0
      );

  float separation = mix(maxSeparation, minSeparation, depth);

  // ...

The fragment's depth ranges from zero to one. When the fragment's view-space y coordinate matches the far clipping plane, the depth is one. When the fragment's view-space y coordinate matches the near clipping plane, the depth is zero. In other words, the depth ranges from zero at the near clipping plane all the way up to one at the far clipping plane.

  // ...

  float separation = mix(maxSeparation, minSeparation, depth);

  // ...

Converting the position to a depth value isn't necessary but it allows you to vary the thickness of the outline based on how far away the fragment is from the camera. Far away fragments get a thinner line while nearer fragments get a thicker outline. This tends to look nicer than a constant thickness since it gives depth to the outline.

Finding The Discontinuities

Edge Finding

  // ...

  float mx = 0.0;

  for (int i = -size; i <= size; ++i) {
    for (int j = -size; j <= size; ++j) {
      // ...
    }
  }

  // ...

Now that you have the current fragment's position, loop through an i by j grid or window surrounding the current fragment.

      // ...

      texCoord =
          ( fragCoord
          + (vec2(i, j) * separation)
          )
        / texSize;

      vec4 positionTemp =
        texture
          ( positionTexture
          , texCoord
          );

      mx = max(mx, abs(position.y - positionTemp.y));

      // ...

With each iteration, find the biggest distance between this fragment's and the surrounding fragments' positions.

smoothstep

  // ...

  float diff = smoothstep(minDistance, maxDistance, mx);

  // ...

Calculate the significance of any difference discovered using the minDistance, maxDistance, and smoothstep. smoothstep returns values from zero to one. The minDistance is the left-most edge. Any difference less than the minimum distance will be zero. The maxDistance is the right-most edge. Any difference greater than the maximum distance will be one. For distances between the edges, the difference will be between zero and one. These values are interpolated along a s-shaped curve.

Line Color

  // ...

  vec3 lineColor = texture(colorTexture, texCoord).rgb * colorModifier;

  // ...

The line color is the current fragment color either darkened or lightened.

Fragment Color

Outlines

  // ...

  fragColor.rgb = vec4(lineColor, diff);

  // ...

The fragment's RGB color is the lineColor and its alpha channel is diff.

Sketchy

Sketchy Outline

For a sketchy outline, you can distort the UV coordinates used to sample the position vectors.

Outline Noise

// ...

uniform sampler2D noiseTexture;

// ...

Start by creating a RGB noise texture. A good size is either 128 by 128 or 512 by 512. Be sure to blur it and make it tileable. This will produce a nice wavy, inky outline.

  // ...

  float noiseScale = 10.0;

  // ...

The noiseScale parameter controls how distorted the outline is. The bigger the noiseScale, the sketchier the line.

  // ...

  vec2 fragCoord = gl_FragCoord.xy;

  vec2 noise  = texture(noiseTexture, fragCoord / textureSize(noiseTexture, 0).xy).rb;
       noise  = noise * 2.0 - 1.0;
       noise *= noiseScale;

  // ...

Sample the noise texture using the current screen/fragment position and the size of the noise texture. Since you're distorting the UV coordinates used to sample the position vectors, you'll only need two of the three color channels. Map the two color channels from [0, 1] to [-1, 1]. Finally, scale the noise by the scale chosen earlier.

  // ...

  vec2 texSize  = textureSize(colorTexture, 0).xy;
  vec2 texCoord = (fragCoord - noise) / texSize;

  vec4 position = texture(positionTexture, texCoord);

  // ...

When sampling the current position, subtract the noise vector from the current fragment's coordinates.

Squiggly Outline

You could instead add it to the current fragment's coordinates which will create more of a squiggly line that loosely follows the geometry.

  // ...

      texCoord =
          (vec2(i, j) * separation + fragCoord + noise)
        / texSize;

      // ...

      vec4 positionTemp =
        texture
          ( positionTexture
          , texCoord
          );

      // ...

When sampling the surrounding positions inside the loop, add the noise vector to the current fragment's coordinates. The rest of the calculations are the same.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/pixelization.html ================================================ Pixelization | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Pixelization

Pixelization

Pixelizing your 3D game can give it a interesting look and possibly save you time by not having to create all of the pixel art by hand. Combine it with the posterization for a true retro look.

  // ...

  int pixelSize = 5;

  // ...

Feel free to adjust the pixel size. The bigger the pixel size, the blockier the image will be.

Pixelization Process

  // ...

  float x = int(gl_FragCoord.x) % pixelSize;
  float y = int(gl_FragCoord.y) % pixelSize;

  x = floor(pixelSize / 2.0) - x;
  y = floor(pixelSize / 2.0) - y;

  x = gl_FragCoord.x + x;
  y = gl_FragCoord.y + y;

  // ...

The technique works by mapping each fragment to the center of its closest, non-overlapping pixel-sized window. These windows are laid out in a grid over the input texture. The center-of-the-window fragments determine the color for the other fragments in their window.

    // ...

    fragColor = texture(colorTexture, vec2(x, y) / texSize);

    // ...

Once you have determined the correct fragment coordinate to use, pull its color from the input texture and assign that to the fragment color.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/posterization.html ================================================ Posterization | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Posterization

Posterization

Posterization or color quantization is the process of reducing the number of unique colors in an image. You can use this shader to give your game a comic book or retro look. Combine it with outlining for a full-on cartoon art style.

There are various different ways to implement posterization. This method works directly with the greyscale values and indirectly with the RGB values of the image. For each fragment, it maps the RGB color to a greyscale value. This greyscale value is then mapped to both its lower and upper level value. The closest level to the original greyscale value is then mapped back to an RGB value This new RGB value becomes the fragment color. I find this method produces nicer results than the more typical methods you'll find.

  // ...

  float levels = 10;

  // ...

The levels parameter controls how many discrete bands or steps there are. This will break up the continuous values from zero to one into chunks. With four levels, 0.0 to 1.0 becomes 0.0, 0.25, 0.5, 0.75, and 1.0.

  // ...

  fragColor = texture(posterizeTexture, texCoord);

  // ...

Sample the current fragment's color.

  // ...

  float greyscale = max(fragColor.r, max(fragColor.g, fragColor.b));

  // ...

Map the RGB values to a greyscale value. In this instance, the greyscale value is the maximum value of the R, G, and B values.

  // ...

  float lower     = floor(greyscale * levels) / levels;
  float lowerDiff = abs(greyscale - lower);

  // ...

Map the greyscale value to its lower level and then calculate the difference between its lower level and itself. For example, if the greyscale value is 0.87 and there are four levels, its lower level is 0.75 and the difference is 0.12.

  // ...

  float upper     = ceil(greyscale * levels) / levels;
  float upperDiff = abs(upper - greyscale);

  // ...

Now calculate the upper level and the difference. Keeping with the example up above, the upper level is 1.0 and the difference is 0.13.

  // ...

  float level      = lowerDiff <= upperDiff ? lower : upper;
  float adjustment = level / greyscale;

  // ...

The closest level is used to calculate the adjustment. The adjustment is the ratio between the quantized and unquantized greyscale value. This adjustment is used to map the quantized greyscale value back to an RGB value.

  // ...

  fragColor.rgb * adjustment;

  // ...

After multiplying rgb by the adjustment, max(r, max(g, b)) will now equal the quantized greyscale value. This maps the quantized greyscale value back to a red, green, and blue vector.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/reference-frames.html ================================================ Reference Frames | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Reference Frames

Before you write any shaders, you should be familiar with the following frames of reference or coordinate systems. All of them boil down to what origin (0, 0, 0) are these coordinates currently relative to? Once you know that, you can then transform them, via some matrix, to some other vector space if need be. Typically, when the output of some shader looks wrong, it's because of some coordinate system mix up.

Model

Model Space

The model or object coordinate system is relative to the origin of the model. This is typically set to the center of the model's geometry in a modeling program like Blender.

World

World Space

The world space is relative to the origin of the scene/level/universe that you've created.

View

View Space

The view or eye coordinate space is relative to the position of the active camera.

Clip

Clip Space

The clip space is relative to the center of the camera's film. All coordinates are now homogeneous, ranging from negative one to one (-1, 1). X and y are parallel with the camera's film and the z coordinate is the depth.

Frustum

Any vertex not within the bounds of the camera's frustum or view volume is clipped or discarded. You can see this happening with the cube towards the back, clipped by the camera's far plane, and the cube off to the side.

Screen

Screen Space

The screen space is (typically) relative to the lower left corner of the screen. X goes from zero to the screen width. Y goes from zero to the screen height.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/render-to-texture.html ================================================ Render To Texture | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Render To Texture

Instead of rendering/drawing/painting directly to the screen, the example code uses a technique called "render to texture". In order to render to a texture, you'll need to set up a framebuffer and bind a texture to it. Multiple textures can be bound to a single framebuffer.

The textures bound to the framebuffer hold the vector(s) returned by the fragment shader. Typically these vectors are color vectors (r, g, b, a) but they could also be position or normal vectors (x, y, z, w). For each bound texture, the fragment shader can output a different vector. For example you could output a vertex's position and normal in a single pass.

Most of the example code dealing with Panda3D involves setting up framebuffer textures. To keep things straightforward, nearly all of the fragment shaders in the example code have only one output. However, you'll want to output as much as you can each render pass to keep your frames per second (FPS) high.

There are two framebuffer texture setups found in the example code.

The first framebuffer texture setup.

The first setup renders the mill scene into a framebuffer texture using a variety of vertex and fragment shaders. This setup will go through each of the mill scene's vertexes and corresponding fragments.

In this setup, the example code performs the following.

  • Stores geometry data (like vertex position or normal) for later use.
  • Stores material data (like the diffuse color) for later use.
  • UV maps the various textures (diffuse, normal, shadow, etc.).
  • Calculates the ambient, diffuse, specular, and emission lighting.

The second framebuffer texture setup.

The second setup is an orthographic camera pointed at a screen-shaped rectangle. This setup will go through just the four vertexes and their corresponding fragments.

In this second setup, the example code performs the following.

  • Manipulates the output of another framebuffer texture.
  • Combines various framebuffer textures into one.

I like to think of this second setup as using layers in GIMP, Krita, or Inkscape.

Tabbing Through Framebuffer Textures

In the example code, you can see the output of a particular framebuffer texture by using the Tab key or the Shift+Tab keys.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/rim-lighting.html ================================================ Rim Lighting | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Rim Lighting

Rim Lighting

Taking inspiration from the fresnel factor, rim lighting targets the rim or silhouette of an object. When combined with cel shading and outlining, it can really complete that cartoon look. You can also use it to highlight objects in the game, making it easier for players to navigate and accomplish tasks.

  // ...

  vec3 eye = normalize(-vertexPosition.xyz);

  // ...

As it was for the fresnel factor, you'll need the eye vector. If your vertex positions are in view space, the eye vector is the negation of the vertex position.

Rim Light

  // ...

  float rimLightIntensity = dot(eye, normal);
        rimLightIntensity = 1.0 - rimLightIntensity;
        rimLightIntensity = max(0.0, rimLightIntensity);

  // ...

The Intensity of the rim light ranges from zero to one. When the eye and normal vector point in the same direction, the rim light intensity is zero. As the two vectors start to point in different directions, the rim light intensity increases until it eventually reaches one when the eye and normal become orthogonal or perpendicular to one another.

Rim Light Power

  // ...

  rimLightIntensity = pow(rimLightIntensity, rimLightPower);

  // ...

You can control the falloff of the rim light using the power function.

  // ...

  rimLightIntensity = smoothstep(0.3, 0.4, rimLightIntensity)

  // ...

step or smoothstep can also be used to control the falloff. This tends to look better when using cel shading. You'll learn more about these functions in later sections.

  // ...

  vec4 rimLight   = rimLightIntensity * diffuse;
       rimLight.a = diffuse.a;

  // ...

What color you use for the rim light is up to you. The demo code multiplies the diffuse light by the rimLightIntensity. This will highlight the silhouette without overexposing it and without lighting any shadowed fragments.

  // ...

  vec4 outputColor     = vec4(0.0);
       outputColor.a   = diffuseColor.a;
       outputColor.rgb =
           ambient.rgb
        +  diffuse.rgb
        + specular.rgb
        + rimLight.rgb
        + emission.rgb;

  // ...

After you've calculated the rim light, add it to the ambient, diffuse, specular, and emission lights.

Source

(C) 2020 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/running-the-demo.html ================================================ Running The Demo | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Running The Demo

Running The Demo

After you've built the example code, you can now run the executable or demo.

./3d-game-shaders-for-beginners

Here's how you run it on Linux or Mac.

3d-game-shaders-for-beginners.exe

Here's how you run it on Windows.

Demo Controls

The demo comes with both keyboard and mouse controls to move the camera around, toggle on and off the different effects, adjust the fog, and view the various different framebuffer textures.

Mouse

You can rotate the scene around by holding down the Left Mouse button and dragging. Hold down the Right Mouse button and drag to move up, down, left, and/or right. To zoom in, roll the Mouse Wheel forward. To zoom out, roll the Mouse Wheel backward.

You can also change the focus point using the mouse. To change the focus point, click anywhere on the scene using the Middle Mouse button.

Keyboard

  • w to rotate the scene down.
  • a to rotate the scene clockwise.
  • s to rotate the scene up.
  • d to rotate the scene counterclockwise.
  • z to zoom in to the scene.
  • x to zoom out of the scene.
  • to move left.
  • to move right.
  • to move up.
  • to move down.

  • 1 to show midday.
  • 2 to show midnight.

  • Delete to toggle the sound.
  • 3 to toggle fresnel.
  • 4 to toggle rim lighting.
  • 5 to toggle particles.
  • 6 to toggle motion blur.
  • 7 to toggle Kuwahara filtering.
  • 8 to toggle cel shading.
  • 9 to toggle lookup table processing.
  • 0 to toggle between Phong and Blinn-Phong.
  • y to toggle SSAO.
  • u to toggle outlining.
  • i to toggle bloom.
  • o to toggle normal mapping.
  • p to toggle fog.
  • h to toggle depth of field.
  • j to toggle posterization.
  • k to toggle pixelization.
  • l to toggle sharpen.
  • n to toggle film grain.
  • m to toggle screen space reflection.
  • , to toggle screen space refraction.
  • . to toggle flow mapping.
  • / to toggle the sun animation.
  • \ to toggle chromatic aberration.

  • r to reset the scene.

  • [ to decrease the fog near distance.
  • Shift+[ to increase the fog near distance.
  • ] to increase the fog far distance.
  • Shift+] to decrease the fog far distance.

  • Shift+- to decrease the amount of foam.
  • - to increase the amount of foam.

  • Shift+= to decrease the relative index of refraction.
  • = to increase the relative index of refraction.

  • Tab to move forward through the framebuffer textures.
  • Shift+Tab to move backward through the framebuffer textures.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/screen-space-reflection.html ================================================ Screen Space Reflection | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Screen Space Reflection (SSR)

Screen Space Reflections

Adding reflections can really ground your scene. Wet and shiny objects spring to life as nothing makes something look wet or shiny quite like reflections. With reflections, you can really sell the illusion of water and metallic objects.

In the lighting section, you simulated the reflected, mirror-like image of the light source. This was the process of rendering the specular reflection. Recall that the specular light was computed using the reflected light direction. Similarly, using screen space reflection or SSR, you can simulate the reflection of other objects in the scene instead of just the light source. Instead of the light ray coming from the source and bouncing off into the camera, the light ray comes from some object in the scene and bounces off into the camera.

SSR works by reflecting the screen image onto itself using only itself. Compare this to cube mapping which uses six screens or textures. In cube mapping, you reflect a ray from some point in your scene to some point on the inside of a cube surrounding your scene. In SSR, you reflect a ray from some point on your screen to some other point on your screen. By reflecting your screen onto itself, you can create the illusion of reflection. This illusion holds for the most part but SSR does fail in some cases as you'll see.

Ray Marching

Screen space reflection uses a technique known as ray marching to determine the reflection for each fragment. Ray marching is the process of iteratively extending or contracting the length or magnitude of some vector in order to probe or sample some space for information. The ray in screen space reflection is the position vector reflected about the normal.

Intuitively, a light ray hits some point in the scene, bounces off, travels in the opposite direction of the reflected position vector, bounces off the current fragment, travels in the opposite direction of the position vector, and hits the camera lens allowing you to see the color of some point in the scene reflected in the current fragment. SSR is the process of tracing the light ray's path in reverse. It tries to find the reflected point the light ray bounced off of and hit the current fragment. With each iteration, the algorithm samples the scene's positions or depths, along the reflection ray, asking each time if the ray intersected with the scene's geometry. If there is an intersection, that position in the scene is a potential candidate for being reflected by the current fragment.

Ideally there would be some analytical method for determining the first intersection point exactly. This first intersection point is the only valid point to reflect in the current fragment. Instead, this method is more like a game of battleship. You can't see the intersections (if there are any) so you start at the base of the reflection ray and call out coordinates as you travel in the direction of the reflection. With each call, you get back an answer of whether or not you hit something. If you do hit something, you try points around that area hoping to find the exact point of intersection.

SSR Ray Marching

Here you see ray marching being used to calculate each fragment's reflected point. The vertex normal is the bright green arrow, the position vector is the bright blue arrow, and the bright red vector is the reflection ray marching through view space.

Vertex Positions

Like SSAO, you'll need the vertex positions in view space. Referrer back to SSAO for details.

Vertex Normals

To compute the reflections, you'll need the vertex normals in view space. Referrer back to SSAO for details.

SSR using normal maps.

Here you see SSR using the normal mapped normals instead of the vertex normals. Notice how the reflection follows the ripples in the water versus the more mirror like reflection shown earlier.

To use the normal maps instead, you'll need to transform the normal mapped normals from tangent space to view space just like you did in the lighting calculations. You can see this being done in normal.frag.

Position Transformations

Position Transformations

Just like SSAO, SSR goes back and forth between the screen and view space. You'll need the camera lens' projection matrix to transform points in view space to clip space. From clip space, you'll have to transform the points again to UV space. Once in UV space, you can sample a vertex/fragment position from the scene which will be the closest position in the scene to your sample. This is the screen space part in screen space reflection since the "screen" is a texture UV mapped over a screen shaped rectangle.

Reflected UV Coordinates

There are a few ways you can implement SSR. The example code starts the reflection process by computing a reflected UV coordinate for each screen fragment. You could skip this part and go straight to computing the reflected color instead, using the final rendering of the scene.

Recall that UV coordinates range from zero to one for both U and V. The screen is just a 2D texture UV mapped over a screen-sized rectangle. Knowing this, the example code doesn't actually need the final rendering of the scene to compute the reflections. It can instead calculate what UV coordinate each screen pixel will eventually use. These calculated UV coordinates can be saved to a framebuffer texture and used later when the scene has been rendered.

Reflected UVs

Here you see the reflected UV coordinates. Without even rendering the scene yet, you can get a good feel for what the reflections will look like.

//...

uniform mat4 lensProjection;

uniform sampler2D positionTexture;
uniform sampler2D normalTexture;

//...

You'll need the camera lens' projection matrix as well as the interpolated vertex positions and normals in view space.

  // ...

  float maxDistance = 15;
  float resolution  = 0.3;
  int   steps       = 10;
  float thickness   = 0.5;

  // ...

Like the other effects, SSR has a few parameters you can adjust. Depending on the complexity of the scene, it may take you awhile to find the right settings. Getting screen space reflections to look just right tends to be difficult when reflecting complex geometry.

The maxDistance parameter controls how far a fragment can reflect. In other words, it controls the maximum length or magnitude of the reflection ray.

The resolution parameter controls how many fragments are skipped while traveling or marching the reflection ray during the first pass. This first pass is to find a point along the ray's direction where the ray enters or goes behind some geometry in the scene. Think of this first pass as the rough pass. Note that the resolution ranges from zero to one. Zero will result in no reflections while one will travel fragment-by-fragment along the ray's direction. A resolution of one can slow down your FPS considerably especially with a large maxDistance.

The steps parameter controls how many iterations occur during the second pass. This second pass is to find the exact point along the reflection ray's direction where the ray immediately hits or intersects with some geometry in the scene. Think of this second pass as the refinement pass.

The thickness controls the cutoff between what counts as a possible reflection hit and what does not. Ideally, you'd like to have the ray immediately stop at some camera-captured position or depth in the scene. This would be the exact point where the light ray bounced off, hit your current fragment, and then bounced off into the camera. Unfortunately the calculations are not always that precise so thickness provides some wiggle room or tolerance. You'll want the thickness to be as small as possible—just a short distance beyond a sampled position or depth.

Larger Thickness

You'll find that as the thickness gets larger, the reflections tend to smear in places.

Smaller Thickness

Going in the other direction, as the thickness gets smaller, the reflections become noisy with tiny little holes and narrow gaps.

  // ...

  vec2 texSize  = textureSize(positionTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 positionFrom     = texture(positionTexture, texCoord);
  vec3 unitPositionFrom = normalize(positionFrom.xyz);
  vec3 normal           = normalize(texture(normalTexture, texCoord).xyz);
  vec3 pivot            = normalize(reflect(unitPositionFrom, normal));

  // ...

Gather the current fragment's position, normal, and reflection about the normal. positionFrom is a vector from the camera position to the current fragment position. normal is a vector pointing in the direction of the interpolated vertex normal for the current fragment. pivot is the reflection ray or vector pointing in the reflected direction of the positionFrom vector. It currently has a length or magnitude of one.

  // ...

  vec4 startView = vec4(positionFrom.xyz + (pivot *           0), 1);
  vec4 endView   = vec4(positionFrom.xyz + (pivot * maxDistance), 1);

  // ...

Calculate the start and end point of the reflection ray in view space.

  // ...

  vec4 startFrag      = startView;
       // Project to screen space.
       startFrag      = lensProjection * startFrag;
       // Perform the perspective divide.
       startFrag.xyz /= startFrag.w;
       // Convert the screen-space XY coordinates to UV coordinates.
       startFrag.xy   = startFrag.xy * 0.5 + 0.5;
       // Convert the UV coordinates to fragment/pixel coordnates.
       startFrag.xy  *= texSize;

  vec4 endFrag      = endView;
       endFrag      = lensProjection * endFrag;
       endFrag.xyz /= endFrag.w;
       endFrag.xy   = endFrag.xy * 0.5 + 0.5;
       endFrag.xy  *= texSize;

  // ...

Project or transform these start and end points from view space to screen space. These points are now fragment positions which correspond to pixel positions on the screen. Now that you know where the ray starts and ends on the screen, you can travel or march along its direction in screen space. Think of the ray as a line drawn on the screen. You'll travel along this line using it to sample the fragment positions stored in the position framebuffer texture.

Screen space versus view space.

Note that you could march the ray through view space but this may under or over sample scene positions found in the position framebuffer texture. Recall that the position framebuffer texture is the size and shape of the screen. Every screen fragment or pixel corresponds to some position captured by the camera. A reflection ray may travel a long distance in view space, but in screen space, it may only travel through a few pixels. You can only sample the screen's pixels for positions so it is inefficient to potentially sample the same pixels over and over again while marching in view space. By marching in screen space, you'll more efficiently sample the fragments or pixels the ray actually occupies or covers.

  // ...

  vec2 frag  = startFrag.xy;
       uv.xy = frag / texSize;

  // ...

The first pass will begin at the starting fragment position of the reflection ray. Convert the fragment position to a UV coordinate by dividing the fragment's coordinates by the position texture's dimensions.

  // ...

  float deltaX    = endFrag.x - startFrag.x;
  float deltaY    = endFrag.y - startFrag.y;

  // ...

Calculate the delta or difference between the X and Y coordinates of the end and start fragments. This will be how many pixels the ray line occupies in the X and Y dimension of the screen.

The reflection ray in screen space.

  // ...

  float useX      = abs(deltaX) >= abs(deltaY) ? 1 : 0;
  float delta     = mix(abs(deltaY), abs(deltaX), useX) * clamp(resolution, 0, 1);

  // ...

To handle all of the various different ways (vertical, horizontal, diagonal, etc.) the line can be oriented, you'll need to keep track of and use the larger difference. The larger difference will help you determine how much to travel in the X and Y direction each iteration, how many iterations are needed to travel the entire line, and what percentage of the line does the current position represent.

useX is either one or zero. It is used to pick the X or Y dimension depending on which delta is bigger. delta is the larger delta of the two X and Y deltas. It is used to determine how much to march in either dimension each iteration and how many iterations to take during the first pass.

  // ...

  vec2  increment = vec2(deltaX, deltaY) / max(delta, 0.001);

  // ...

Calculate how much to increment the X and Y position by using the larger of the two deltas. If the two deltas are the same, each will increment by one each iteration. If one delta is larger than the other, the larger delta will increment by one while the smaller one will increment by less than one. This assumes the resolution is one. If the resolution is less than one, the algorithm will skip over fragments.

startFrag  = ( 1,  4)
endFrag    = (10, 14)

deltaX     = (10 - 1) = 9
deltaY     = (14 - 4) = 10

resolution = 0.5

delta      = 10 * 0.5 = 5

increment  = (deltaX, deltaY) / delta
           = (     9,     10) / 5
           = ( 9 / 5,      2)

For example, say the resolution is 0.5. The larger dimension will increment by two fragments instead of one.

  // ...

  float search0 = 0;
  float search1 = 0;

  // ...

To move from the start fragment to the end fragment, the algorithm uses linear interpolation.

current position x = (start x) * (1 - search1) + (end x) * search1;
current position y = (start y) * (1 - search1) + (end y) * search1;

search1 ranges from zero to one. When search1 is zero, the current position is the start fragment. When search1 is one, the current position is the end fragment. For any other value, the current position is somewhere between the start and end fragment.

search0 is used to remember the last position on the line where the ray missed or didn't intersect with any geometry. The algorithm will later use search0 in the second pass to help refine the point at which the ray touches the scene's geometry.

  // ...

  int hit0 = 0;
  int hit1 = 0;

  // ...

hit0 indicates there was an intersection during the first pass. hit1 indicates there was an intersection during the second pass.

  // ...

  float viewDistance = startView.y;
  float depth        = thickness;

  // ...

The viewDistance value is how far away from the camera the current point on the ray is. Recall that for Panda3D, the Y dimension goes in and out of the screen in view space. For other systems, the Z dimension goes in and out of the screen in view space. In any case, viewDistance is how far away from the camera the ray currently is. Note that if you use the depth buffer, instead of the vertex positions in view space, the viewDistance would be the Z depth.

Make sure not to confuse the viewDistance value with the Y dimension of the line being traveled across the screen. The viewDistance goes from the camera into scene while the Y dimension of the line travels up or down the screen.

The depth is the view distance difference between the current ray point and scene position. It tells you how far behind or in front of the scene the ray currently is. Remember that the scene positions are the interpolated vertex positions stored in the position framebuffer texture.

  // ...

  for (i = 0; i < int(delta); ++i) {

  // ...

You can now begin the first pass. The first pass runs while i is less than the delta value. When i reaches delta, the algorithm has traveled the entire length of the line. Remember that delta is the larger of the two X and Y deltas.

Screen Space Transformations

    // ...

    frag      += increment;
    uv.xy      = frag / texSize;
    positionTo = texture(positionTexture, uv.xy);

    // ...

Advance the current fragment position closer to the end fragment. Use this new fragment position to look up a scene position stored in the position framebuffer texture.

    // ...

    search1 =
      mix
        ( (frag.y - startFrag.y) / deltaY
        , (frag.x - startFrag.x) / deltaX
        , useX
        );

    // ...

Calculate the percentage or portion of the line the current fragment represents. If useX is zero, use the Y dimension of the line. If useX is one, use the X dimension of the line.

When frag equals startFrag, search1 equals zero since frag - startFrag is zero. When frag equals endFrag, search1 is one since frag - startFrag equals delta.

search1 is the percentage or portion of the line the current position represents. You'll need this to interpolate between the ray's view-space start and end distances from the camera.

    // ...

    viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1);

    // ...

Using search1, interpolate the view distance (distance from the camera in view space) for the current position you're at on the reflection ray.

// Incorrect.
viewDistance = mix(startView.y, endView.y, search1);

// Correct.
viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1);

You may be tempted to just interpolate between the view distances of the start and end view-space positions but this will give you the wrong view distance for the current position on the reflection ray. Instead, you'll need to perform perspective-correct interpolation which you see here.

    // ...

    depth        = viewDistance - positionTo.y;

    // ...

Calculate the difference between the ray's view distance at this point and the sampled view distance of the scene at this point.

    // ...

    if (depth > 0 && depth < thickness) {
      hit0 = 1;
      break;
    } else {
      search0 = search1;
    }

    // ...

If the difference is between zero and the thickness, this is a hit. Set hit0 to one and exit the first pass. If the difference is not between zero and the thickness, this is a miss. Set search0 to equal search1 to remember this position as the last known miss. Continue marching the ray towards the end fragment.

  // ...

  search1 = search0 + ((search1 - search0) / 2);

  // ...

At this point you have finished the first pass. Set the search1 position to be halfway between the position of the last miss and the position of the last hit.

  // ...

  steps *= hit0;

  for (i = 0; i < steps; ++i) {

  // ...

You can now begin the second pass. If the reflection ray didn't hit anything in the first pass, skip the second pass.

    // ...

    frag       = mix(startFrag.xy, endFrag.xy, search1);
    uv.xy      = frag / texSize;
    positionTo = texture(positionTexture, uv.xy);

    // ...

As you did in the first pass, use the current position on the ray line to sample a position from the scene.

    // ...

    viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1);
    depth        = viewDistance - positionTo.y;

    // ...

Interpolate the view distance for the current ray line position and calculate the camera distance difference between the ray at this point and the scene.

    // ...

    if (depth > 0 && depth < thickness) {
      hit1 = 1;
      search1 = search0 + ((search1 - search0) / 2);
    } else {
      float temp = search1;
      search1 = search1 + ((search1 - search0) / 2);
      search0 = temp;
    }

    // ...

If the depth is within bounds, this is a hit. Set hit1 to one and set search1 to be halfway between the last known miss position and this current hit position. If the depth is not within bounds, this is a miss. Set search1 to be halfway between this current miss position and the last known hit position. Move search0 to this current miss position. Continue this back and forth search while i is less than steps.

  // ...

  float visibility =
      hit1

  // ...

You're now done with the second and final pass but before you can output the reflected UV coordinates, you'll need to calculate the visibility of the reflection. The visibility ranges from zero to one. If there wasn't a hit in the second pass, the visibility is zero.

  // ...

    * positionTo.w

  // ...

If the reflected scene position's alpha or w component is zero, the visibility is zero. Note that if w is zero, there was no scene position at that point.

Reflection ray pointing towards the camera position.

  // ...

    * ( 1
      - max
         ( dot(-unitPositionFrom, pivot)
         , 0
         )
      )

  // ...

One of the ways in which screen space reflection can fail is when the reflection ray points in the general direction of the camera. If the reflection ray points towards the camera and hits something, it's most likely hitting the back side of something facing away from the camera.

To handle this failure case, you'll need to gradually fade out the reflection based on how much the reflection vector points to the camera's position. If the reflection vector points directly in the opposite direction of the position vector, the visibility is zero. Any other direction results in the visibility being greater than zero.

Remember to normalize both vectors when taking the dot product. unitPositionFrom is the normalized position vector. It has a length or magnitude of one.

  // ...

    * ( 1
      - clamp
          ( depth / thickness
          , 0
          , 1
          )
      )

  // ...

As you sample scene positions along the reflection ray, you're hoping to find the exact point where the reflection ray first intersects with the scene's geometry. Unfortunately, you may not find this particular point. Fade out the reflection the further it is from the intersection point you did find.

  // ...

    * ( 1
      - clamp
          (   length(positionTo - positionFrom)
            / maxDistance
          , 0
          , 1
          )
      )

  // ...

Fade out the reflection based on how far way the reflected point is from the initial starting point. This will fade out the reflection instead of it ending abruptly as it reaches maxDistance.

Reflection ray exiting the frustum.

  // ...

    * (uv.x < 0 || uv.x > 1 ? 0 : 1)
    * (uv.y < 0 || uv.y > 1 ? 0 : 1);

  // ...

If the reflected UV coordinates are out of bounds, set the visibility to zero. This occurs when the reflection ray travels outside the camera's frustum.

  visibility = clamp(visibility, 0, 1);

  uv.ba = vec2(visibility);

Set the blue and alpha component to the visibility as the UV coordinates only need the RG or XY components of the final vector.

  // ...

  fragColor = uv;

  // ...

The final fragment color is the reflected UV coordinates and the visibility.

Specular Map

Specular Map

In addition to the reflected UV coordinates, you'll also need a specular map. The example code creates one using the fragment's material specular properties.

// ...

#define MAX_SHININESS 127.75

uniform struct
  { vec3 specular
  ; float shininess
  ;
  } p3d_Material;

out vec4 fragColor;

void main() {
  fragColor =
    vec4
      ( p3d_Material.specular
      , clamp(p3d_Material.shininess / MAX_SHININESS, 0, 1)
      );
}

The specular fragment shader is quite simple. Using the fragment's material, the shader outputs the specular color and uses the alpha channel for the shininess. The shininess is mapped to a range of zero to one. In Blender, the maximum specular hardness or shininess is 511. When exporting from Blender to Panda3D, 511 is exported as 127.75. Feel free to adjust the shininess to range of zero to one however you see fit for your particular stack.

The example code generates a specular map from the material specular properties but you could create one in GIMP, for example, and attach that as a texture to your 3D model. For instance, say your 3D treasure chest has shiny brackets on it but nothing else should reflect the environment. You can paint the brackets some shade of gray and the rest of the treasure chest black. This will mask off the brackets, allowing your shader to render the reflections on only the brackets and nothing else.

Scene Colors

Scene Colors

You'll need to render the parts of the scene you wish to reflect and store this in a framebuffer texture. This is typically just the scene without any reflections.

Reflected Scene Colors

Reflected Scene Colors

Here you see the reflected colors saved to a framebuffer texture.

// ...

uniform sampler2D uvTexture;
uniform sampler2D colorTexture;

// ...

Once you have the reflected UV coordinates, looking up the reflected colors is fairly easy. You'll need the reflected UV coordinates texture and the color texture containing the colors you wish to reflect.

  // ...

  vec2 texSize  = textureSize(uvTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 uv    = texture(uvTexture,    texCoord);
  vec4 color = texture(colorTexture, uv.xy);

  // ...

Using the UV coordinates for the current fragment, look up the reflected color.

  // ...

  float alpha = clamp(uv.b, 0, 1);

  // ...

Recall that the reflected UV texture stored the visibility in the B or blue component. This is the alpha channel for the reflected colors framebuffer texture.

  // ...

  fragColor = vec4(mix(vec3(0), color.rgb, alpha), alpha);

  // ...

The fragment color is a mix between no reflection and the reflected color based on the visibility. The visibility was computed during the reflected UV coordinates step.

Blurred Reflected Scene Colors

Blurred Reflected Scene Colors

Now blur the reflected scene colors and store this in a framebuffer texture. The blurring is done using a box blur. Refer to the SSAO blurring step for details.

The blurred reflected colors are used for surfaces that have a less than mirror like finish. These surfaces have tiny little hills and valleys that tend to diffuse or blur the reflection. I'll cover this more during the roughness calculation.

Reflections

// ...

uniform sampler2D colorTexture;
uniform sampler2D colorBlurTexture;
uniform sampler2D specularTexture;

// ...

To generate the final reflections, you'll need the three framebuffer textures computed earlier. You'll need the reflected colors, the blurred reflected colors, and the specular map.

  // ...

  vec4 specular  = texture(specularTexture,  texCoord);
  vec4 color     = texture(colorTexture,     texCoord);
  vec4 colorBlur = texture(colorBlurTexture, texCoord);

  // ...

Look up the specular amount and shininess, the reflected scene color, and the blurred reflected scene color.

  // ...

  float specularAmount = dot(specular.rgb, vec3(1)) / 3;

  if (specularAmount <= 0) { fragColor = vec4(0); return; }

  // ...

Map the specular color to a greyscale value. If the specular amount is none, set the frag color to nothing and return.

Later on, you'll multiply the final reflection color by the specular amount. Multiplying by the specular amount allows you to control how much a material reflects its environment simply by brightening or darkening the greyscale value in the specular map.

  dot(specular.rgb, vec3(1)) == (specular.r + specular.g + specular.b);

Using the dot product to produce the greyscale value is just a short way of summing the three color components.

  // ...

  float roughness = 1 - min(specular.a, 1);

  // ...

Calculate the roughness based on the shininess value set during the specular map step. Recall that the shininess value was saved in the alpha channel of the specular map. The shininess determined how spread out or blurred the specular reflection was. Similarly, the roughness determines how blurred the reflection is. A roughness of one will produce the blurred reflection color. A roughness of zero will produce the non-blurred reflection color. Doing it this way allows you to control how blurred the reflection is just by changing the material's shininess value.

The example code generates a roughness map from the material specular properties but you could create one in GIMP, for example, and attach that as a texture to your 3D model. For instance, say you have a tiled floor that has polished tiles and scratched up tiles. The polished tiles could be painted a more translucent white while the scratched up tiles could be painted a more opaque white. The more translucent/transparent the greyscale value, the more the shader will use the blurred reflected color. The scratched tiles will have a blurry reflection while the polished tiles will have a mirror like reflection.

  // ...

  fragColor = mix(color, colorBlur, roughness) * specularAmount;

  // ...

Mix the reflected color and blurred reflected color based on the roughness. Multiply that vector by the specular amount and then set that value as the fragment color.

The reflection color is a mix between the reflected scene color and the blurred reflected scene color based on the roughness. A high roughness will produce a blurry reflection meaning the surface is rough. A low roughness will produce a clear reflection meaning the surface is smooth.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/screen-space-refraction.html ================================================ Screen Space Refraction | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Screen Space Refraction (SSR)

Screen Space Refraction

Screen space refraction, much like screen space reflection, adds a touch of realism you can't find anywhere else. Glass, plastic, water, and other transparent/translucent materials spring to life.

Screen space reflection and screen space refraction work almost identically expect for one major difference. Instead of using the reflected vector, screen space refraction uses the refracted vector. It's a slight change in code but a big difference visually.

Vertex Positions

Like SSAO, you'll need the vertex positions in view space. Referrer back to SSAO for details.

However, unlike SSAO, you'll need the scene's vertex positions with and without the refractive objects. Refractive surfaces are translucent, meaning you can see through them. Since you can see through them, you'll need the vertex positions behind the refractive surface. Having both the foreground and background vertex positions will allow you to calculate UV coordinates and depth.

Vertex Normals

To compute the refractions, you'll need the scene's foreground vertex normals in view space. The background vertex normals aren't needed unless you need to incorporate the background surface detail while calculating the refracted UV coordinates and distances. Referrer back to SSAO for details.

Normal maps versus no normal maps.

Here you see the water refracting the light with and without normal maps. If available, be sure to use the normal mapped normals instead of the vertex normals. The smoother and flatter the surface, the harder it is to tell the light is being refracted. There will be some distortion but not enough to make it worthwhile.

To use the normal maps instead, you'll need to transform the normal mapped normals from tangent space to view space just like you did in the lighting calculations. You can see this being done in normal.frag.

Position Transformations

Position Transformations

Just like SSAO and screen space reflection, screen space refraction goes back and forth between the screen and view space. You'll need the camera lens' projection matrix to transform points in view space to clip space. From clip space, you'll have to transform the points again to UV space. Once in UV space, you can sample a vertex/fragment position from the scene which will be the closest position in the scene to your sample. This is the screen space part in screen space refraction since the "screen" is a texture UV mapped over a screen shaped rectangle.

Refracted UV Coordinates

Recall that UV coordinates range from zero to one for both U and V. The screen is just a 2D texture UV mapped over a screen-sized rectangle. Knowing this, the example code doesn't actually need the final rendering of the scene to compute the refraction. It can instead calculate what UV coordinate each screen pixel will eventually use. These calculated UV coordinates can be saved to a framebuffer texture and used later when the scene has been rendered.

The process of refracting the UV coordinates is very similar to the process of reflecting the UV coordinates. Below are the adjustments you'll need to turn reflection into refraction.

// ...

uniform sampler2D positionFromTexture;
uniform sampler2D positionToTexture;
uniform sampler2D normalFromTexture;

// ...

Reflection only deals with what is in front of the reflective surface. Refraction, however, deals with what is behind the refractive surface. To accommodate this, you'll need both the vertex positions of the scene with the refracting surfaces taken out and the vertex positions of the scene with the refracting surfaces left in.

Without and with refractive surfaces.

positionFromTexture are the scene's vertex positions with the refracting surfaces left in. positionToTexture are the scene's vertex positions with the refracting surfaces taken out. normalFromTexture are the scene's vertex normals with the refraction surfaces left in. There's no need for the vertex normals behind the refractive surfaces unless you want to incorporate the surface detail for the background geometry.

// ...

uniform vec2 rior;

// ...

Refraction has one more adjustable parameter than reflection. rior is the relative index of refraction or relative refractive index. It is the ratio of the refraction indexes of two mediums. So for example, going from water to air is 1 / 1.33 ≈ 0.75. The numerator is the refractive index of the medium the light is leaving and the denominator is the refractive index of the medium the light is entering. An rior of one means the light passes right through without being refracted or bent. As rior grows, the refraction will become a reflection.

There's no requirement that rior must adhere to the real world. The demo uses 1.05. This is completely unrealistic (light does not travel faster through water than air) but the realistic setting produced too many artifacts. In the end, the distortion only has to be believable—not realistic.

Adjusting the relative index of refraction.

rior values above one tend to elongate the refraction while numbers below one tend to shrink the refraction.

As it was with screen space reflection, the screen doesn't have the entire geometry of the scene. A refracted ray may march through the screen space and never hit a captured surface. Or it may hit a surface but it's the backside not captured by the camera. When this happened during reflection, the fragment was left blank. This indicated no reflection or not enough information to determine a reflection. Leaving the fragment blank was fine for reflection since the reflective surface would fill in the gaps.

Refraction Holes

For refraction, however, we must set the fragment to some UV. If the fragment is left blank, the refractive surface will contain holes that let the detail behind it come through. This would be okay for a completely transparent surface but usually the refractive surface will have some tint to it, reflection, etc.

  // ...

  vec2 texSize  = textureSize(positionFromTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 uv = vec4(texCoord.xy, 1, 1);

  // ...

The best choice is to select the UV as if the rior was one. This will leave the UV coordinate unchanged, allowing the background to show through instead of there being a hole in the refractive surface.

Refraction UV Map

Here you see the refracted UV texture for the mill scene. The wheel and waterway disturb what is otherwise a smooth gradient. The disruptions shift the UV coordinates from their screen position to their refracted screen position.

  // ...

  vec3 unitPositionFrom = normalize(positionFrom.xyz);
  vec3 normalFrom       = normalize(texture(normalFromTexture, texCoord).xyz);
  vec3 pivot            = normalize(refract(unitPositionFrom, normalFrom, rior.x));

  // ...

The most important difference is the calculation of the refracted vector versus the reflected vector. Both use the unit position and normal but refract takes an additional parameter specifying the relative refractive index.

    // ...

    frag      += increment;
    uv.xy      = frag / texSize;
    positionTo = texture(positionToTexture, uv.xy);

    // ...

The positionTo, sampled by the uv coordinates, uses the positionToTexture. For reflection, you only need one framebuffer texture containing the scene's interpolated vertex positions in view space. However, for refraction, positionToTexture contains the vertex positions of the scene minus the refractive surfaces since the refraction ray typically goes behind the surface. If positionFromTexture and positionToTexture were the same for refraction, the refracted ray would hit the refractive surface instead of what is behind it.

Refraction Mask

Material Specular

You'll need a mask to filter out the non-refractive parts of the scene. This mask will determine which fragment does and does not receive a refracted color. You could use this mask during the refracted UV calculation step or later when you actually sample the colors at the refracted UV coordinates.

The mill scene uses the models' material specular as a mask. For the demo's purposes, the specular map is sufficient but you may want to use a more specialized map. Refer back to screen space reflection for how to render the specular map.

Background Colors

Background Colors

You'll need to render the parts of the scene behind the refractive objects. This can be done by hiding the refractive objects and then rendering the scene to a framebuffer texture.

Foreground Colors

Foreground Colors

// ...

uniform sampler2D uvTexture;
uniform sampler2D refractionMaskTexture;
uniform sampler2D positionFromTexture;
uniform sampler2D positionToTexture;
uniform sampler2D backgroundColorTexture;

// ...

To render the actual refractions or foreground colors, you'll need the refracted UV coordinates, refraction mask, the foreground and background vertex positions, and the background colors.

  // ...

  vec3  tintColor = vec3(0.27, 0.58, 0.92, 0.3);
  float depthMax  = 2;

  // ...

tintColor and depthMax are adjustable parameters. tintColor colorizes the background color. depthMax ranges from zero to infinity. When the distance between the foreground and background position reaches depthMax, the foreground color will be the fully tinted background color. At distance zero, the foreground will be the background color.

  // ...

  vec2 texSize  = textureSize(backgroundColorTexture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  vec4 uv              = texture(uvTexture,              texCoord);
  vec4 mask            = texture(maskTexture,            texCoord);
  vec4 positionFrom    = texture(positionFromTexture,    texCoord);
  vec4 positionTo      = texture(positionToTexture,      uv.xy);
  vec4 backgroundColor = texture(backgroundColorTexture, uv.xy);

  if (refractionMask.r <= 0) { fragColor = vec4(0); return; }

  // ...

Pull out the uv coordinates, mask, background position, foreground position, and the background color.

If the refraction mask is turned off for this fragment, return nothing.

  // ...

  float depth   = length(positionTo.xyz - positionFrom.xyz);
  float mixture = clamp(depth / depthMax, 0, 1);

  vec3 shallowColor    = backgroundColor.rgb;
  vec3 deepColor       = mix(shallowColor, tintColor.rgb, tintColor.a);
  vec3 foregroundColor = mix(shallowColor, deepColor,     mixture);

  // ...

Refraction Depth

Calculate the depth or distance between the foreground position and the background position. At zero depth, the foreground color will be the shallow color. At depthMax, the foreground color will be the deep color. The deep color is the background color tinted with tintColor.

  // ...

  fragColor = mix(vec4(0), vec4(foregroundColor, 1), uv.b);

  // ...

Recall that the blue channel, in the refracted UV texture, is set to the visibility. The visibility declines as the refracted ray points back at the camera. While the visibility should always be one, it is put here for completeness. As the visibility lessens, the fragment color will receive less and less of the foreground color.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/setup.html ================================================ Setup | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Setup

Setup

Below is the setup used to develop and test the example code.

Environment

The example code was developed and tested using the following environment.

  • Linux manjaro 5.10.42-1-MANJARO
  • OpenGL renderer string: GeForce GTX 970/PCIe/SSE2
  • OpenGL version string: 4.6.0 NVIDIA 465.31
  • g++ (GCC) 11.1.0
  • Panda3D 1.10.9

Materials

Each Blender material used to build mill-scene.egg has five textures in the following order.

  • Diffuse
  • Normal
  • Specular
  • Reflection
  • Refraction

By having the same maps in the same positions for all models, the shaders can be generalized, reducing the need to duplicate code.

If an object uses its vertex normals, a "flat blue" normal map is used.

A flat normal map.

Here is an example of a flat normal map. The only color it contains is flat blue (red = 128, green = 128, blue = 255). This color represents a unit (length one) normal pointing in the positive z-axis (0, 0, 1).

(0, 0, 1) =
  ( round((0 * 0.5 + 0.5) * 255)
  , round((0 * 0.5 + 0.5) * 255)
  , round((1 * 0.5 + 0.5) * 255)
  ) =
    (128, 128, 255) =
      ( round(128 / 255 * 2 - 1)
      , round(128 / 255 * 2 - 1)
      , round(255 / 255 * 2 - 1)
      ) =
        (0, 0, 1)

Here you see the unit normal (0, 0, 1) converted to flat blue (128, 128, 255) and flat blue converted to the unit normal. You'll learn more about this in the normal mapping technique.

Specular Map

Up above is one of the specular maps used. The red and blue channel work to control the amount of specular reflection seen based on the camera angle. The green channel controls the shininess factor. You'll learn more about this in the lighting and fresnel factor sections.

The reflection and refraction textures mask off the objects that are either reflective, refractive, or both. For the reflection texture, the red channel controls the amount of reflection and the green channel controls how clear or blurry the reflection is.

Panda3D

The example code uses Panda3D as the glue between the shaders. This has no real influence over the techniques described, meaning you'll be able to take what you learn here and apply it to your stack or game engine of choice. Panda3D does provide some conveniences. I have pointed these out so you can either find an equivalent convenience provided by your stack or replicate it yourself, if your stack doesn't provide something equivalent.

Three Panda3D configurations were changed for the purposes of the demo program. You can find these in config.prc. The configurations changed were gl-coordinate-system default, textures-power-2 down, and textures-auto-power-2 1. Refer to the Panda3D configuration page in the manual for more details.

Panda3D defaults to a z-up, right-handed coordinate system while OpenGL uses a y-up, right-handed system. gl-coordinate-system default keeps you from having to translate between the two inside your shaders. textures-auto-power-2 1 allows us to use texture sizes that are not a power of two if the system supports it. This comes in handy when doing SSAO and other screen/window sized related techniques since the screen/window size is usually not a power of two. textures-power-2 down downsizes our textures to a power of two if the system only supports texture sizes being a power of two.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/sharpen.html ================================================ Sharpen | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Sharpen

Sharpen

The sharpen effect increases the contrast at the edges of the image. This comes in handy when your graphics are bit too soft.

  // ...

  float amount = 0.8;

  // ...

You can control how sharp the result is by adjusting the amount. An amount of zero leaves the image untouched. Try negative values for an odd look.

  // ...

  float neighbor = amount * -1;
  float center   = amount * 4 + 1;

  // ...

Neighboring fragments are multiplied by amount * -1. The current fragment is multiplied by amount * 4 + 1.

  // ...

  vec3 color =
        texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 1) / texSize).rgb
      * neighbor

      + texture(sharpenTexture, vec2(gl_FragCoord.x - 1, gl_FragCoord.y + 0) / texSize).rgb
      * neighbor
      + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 0) / texSize).rgb
      * center
      + texture(sharpenTexture, vec2(gl_FragCoord.x + 1, gl_FragCoord.y + 0) / texSize).rgb
      * neighbor

      + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y - 1) / texSize).rgb
      * neighbor
      ;

  // ...

The neighboring fragments are up, down, left, and right. After multiplying both the neighbors and the current fragment by their particular values, sum the result.

    // ...

    fragColor = vec4(color, texture(sharpenTexture, texCoord).a);

    // ...

This sum is the final fragment color.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/ssao.html ================================================ SSAO | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Screen Space Ambient Occlusion (SSAO)

SSAO

SSAO is one of those effects you never knew you needed and can't live without once you have it. It can take a scene from mediocre to wow! For fairly static scenes, you can bake ambient occlusion into a texture but for more dynamic scenes, you'll need a shader. SSAO is one of the more fairly involved shading techniques, but once you pull it off, you'll feel like a shader master.

By using only a handful of textures, SSAO can approximate the ambient occlusion of a scene. This is faster than trying to compute the ambient occlusion by going through all of the scene's geometry. These handful of textures all originate in screen space giving screen space ambient occlusion its name.

Inputs

The SSAO shader will need the following inputs.

  • Vertex position vectors in view space.
  • Vertex normal vectors in view space.
  • Sample vectors in tangent space.
  • Noise vectors in tangent space.
  • The camera lens' projection matrix.

Vertex Positions

Panda3D Vertex Positions

Storing the vertex positions into a framebuffer texture is not a necessity. You can recreate them from the camera's depth buffer. This being a beginners guide, I'll avoid this optimization and keep it straight forward. Feel free to use the depth buffer, however, for your implementation.

PT(Texture) depthTexture =
  new Texture("depthTexture");
depthTexture->set_format
  ( Texture::Format::F_depth_component32
  );

PT(GraphicsOutput) depthBuffer =
  graphicsOutput->make_texture_buffer
    ( "depthBuffer"
    , 0
    , 0
    , depthTexture
    );
depthBuffer->set_clear_color
  ( LVecBase4f(0, 0, 0, 0)
  );

NodePath depthCameraNP =
  window->make_camera();
DCAST(Camera, depthCameraNP.node())->set_lens
  ( window->get_camera(0)->get_lens()
  );
PT(DisplayRegion) depthBufferRegion =
  depthBuffer->make_display_region
    ( 0
    , 1
    , 0
    , 1
    );
depthBufferRegion->set_camera(depthCameraNP);

If you do decide to use the depth buffer, here's how you can set it up using Panda3D.

in vec4 vertexPosition;

out vec4 fragColor;

void main() {
  fragColor = vertexPosition;
}

Here's the simple shader used to render out the view space vertex positions into a framebuffer texture. The more involved work is setting up the framebuffer texture such that the fragment vector components it receives are not clamped to [0, 1] and that each one has a high enough precision (a high enough number of bits). For example, if a particular interpolated vertex position is <-139.444444566, 0.00000034343, 2.5>, you don't want it stored into the texture as <0.0, 0.0, 1.0>.

  // ...

  FrameBufferProperties fbp = FrameBufferProperties::get_default();

  // ...

  fbp.set_rgba_bits(32, 32, 32, 32);
  fbp.set_rgb_color(true);
  fbp.set_float_color(true);

  // ...

Here's how the example code sets up the framebuffer texture to store the vertex positions. It wants 32 bits per red, green, blue, and alpha components and disables clamping the values to [0, 1] The set_rgba_bits(32, 32, 32, 32) call sets the bits and also disables the clamping.

  glTexImage2D
    ( GL_TEXTURE_2D
    , 0
    , GL_RGB32F
    , 1200
    , 900
    , 0
    , GL_RGB
    , GL_FLOAT
    , nullptr
    );

Here's the equivalent OpenGL call. GL_RGB32F sets the bits and also disables the clamping.

If the color buffer is fixed-point, the components of the source and destination values and blend factors are each clamped to [0, 1] or [−1, 1] respectively for an unsigned normalized or signed normalized color buffer prior to evaluating the blend equation. If the color buffer is floating-point, no clamping occurs.

OpenGL Vertex Positions

Here you see the vertex positions with y being the up vector.

Recall that Panda3D sets z as the up vector but OpenGL uses y as the up vector. The position shader outputs the vertex positions with z being up since Panda3D was configured with gl-coordinate-system default.

Vertex Normals

Panda3d Vertex Normals

You'll need the vertex normals to correctly orient the samples you'll take in the SSAO shader. The example code generates multiple sample vectors distributed in a hemisphere but you could use a sphere and do away with the need for normals all together.

in vec3 vertexNormal;

out vec4 fragColor;

void main() {
  vec3 normal = normalize(vertexNormal);

  fragColor = vec4(normal, 1);
}

Like the position shader, the normal shader is simple as well. Be sure to normalize the vertex normal and remember that they are in view space.

OpenGL Vertex Normals

Here you see the vertex normals with y being the up vector.

Recall that Panda3D sets z as the up vector but OpenGL uses y as the up vector. The normal shader outputs the vertex positions with z being up since Panda3D was configured with gl-coordinate-system default.

SSAO using the normal maps.

Here you see SSAO being used with the normal maps instead of the vertex normals. This adds an extra level of detail and will pair nicely with the normal mapped lighting.

    // ...

    normal =
      normalize
        ( normalTex.rgb
        * 2.0
        - 1.0
        );
    normal =
      normalize
        ( mat3
            ( tangent
            , binormal
            , vertexNormal
            )
        * normal
        );

    // ...

To use the normal maps instead, you'll need to transform the normal mapped normals from tangent space to view space just like you did in the lighting calculations.

Samples

To determine the amount of ambient occlusion for any particular fragment, you'll need to sample the surrounding area. The more samples you use, the better the approximation at the cost of performance.

  // ...

  for (int i = 0; i < numberOfSamples; ++i) {
    LVecBase3f sample =
      LVecBase3f
        ( randomFloats(generator) * 2.0 - 1.0
        , randomFloats(generator) * 2.0 - 1.0
        , randomFloats(generator)
        ).normalized();

    float rand = randomFloats(generator);
    sample[0] *= rand;
    sample[1] *= rand;
    sample[2] *= rand;

    float scale = (float) i / (float) numberOfSamples;
    scale = lerp(0.1, 1.0, scale * scale);
    sample[0] *= scale;
    sample[1] *= scale;
    sample[2] *= scale;

    ssaoSamples.push_back(sample);
  }

  // ...

The example code generates a number of random samples distributed in a hemisphere. These ssaoSamples will be sent to the SSAO shader.

    LVecBase3f sample =
      LVecBase3f
        ( randomFloats(generator) * 2.0 - 1.0
        , randomFloats(generator) * 2.0 - 1.0
        , randomFloats(generator) * 2.0 - 1.0
        ).normalized();

If you'd like to distribute your samples in a sphere instead, change the random z component to range from negative one to one.

Noise

  // ...

  for (int i = 0; i < numberOfNoise; ++i) {
    LVecBase3f noise =
      LVecBase3f
        ( randomFloats(generator) * 2.0 - 1.0
        , randomFloats(generator) * 2.0 - 1.0
        , 0.0
        );

    ssaoNoise.push_back(noise);
  }

  // ...

To get a good sweep of the sampled area, you'll need to generate some noise vectors. These noise vectors will randomly tilt the hemisphere around the current fragment.

Ambient Occlusion

SSAO Texture

SSAO works by sampling the view space around a fragment. The more samples that are below a surface, the darker the fragment color. These samples are positioned at the fragment and pointed in the general direction of the vertex normal. Each sample is used to look up a position in the position framebuffer texture. The position returned is compared to the sample. If the sample is farther away from the camera than the position, the sample counts towards the fragment being occluded.

SSAO Sampling

Here you see the space above the surface being sampled for occlusion.

  // ...

  float radius    = 1;
  float bias      = 0.01;
  float magnitude = 1.5;
  float contrast  = 1.5;

  // ...

Like some of the other techniques, the SSAO shader has a few control knobs you can tweak to get the exact look you're going for. The bias adds to the sample's distance from the camera. You can use the bias to combat "acne". The radius increases or decreases the coverage area of the sample space. The magnitude either lightens or darkens the occlusion map. The contrast either washes out or increases the starkness of the occlusion map.

  // ...

  vec4 position =           texture(positionTexture, texCoord);
  vec3 normal   = normalize(texture(normalTexture,   texCoord).xyz);

  int  noiseX = int(gl_FragCoord.x - 0.5) % 4;
  int  noiseY = int(gl_FragCoord.y - 0.5) % 4;
  vec3 random = noise[noiseX + (noiseY * 4)];

  // ...

Retrieve the position, normal, and random vector for later use. Recall that the example code created a set number of random vectors. The random vector is chosen based on the current fragment's screen position.

  // ...

  vec3 tangent  = normalize(random - normal * dot(random, normal));
  vec3 binormal = cross(normal, tangent);
  mat3 tbn      = mat3(tangent, binormal, normal);

  // ...

Using the random and normal vectors, assemble the tangent, binormal, and normal matrix. You'll need this matrix to transform the sample vectors from tangent space to view space.

  // ...

  float occlusion = NUM_SAMPLES;

  for (int i = 0; i < NUM_SAMPLES; ++i) {
    // ...
  }

  // ...

With the matrix in hand, the shader can now loop through the samples, subtracting how many are not occluded.

    // ...

    vec3 samplePosition = tbn * samples[i];
         samplePosition = position.xyz + samplePosition * radius;

    // ...

Using the matrix, position the sample near the vertex/fragment position and scale it by the radius.

    // ...

    vec4 offsetUV      = vec4(samplePosition, 1.0);
         offsetUV      = lensProjection * offsetUV;
         offsetUV.xyz /= offsetUV.w;
         offsetUV.xy   = offsetUV.xy * 0.5 + 0.5;

    // ...

Using the sample's position in view space, transform it from view space to clip space to UV space.

-1 * 0.5 + 0.5 = 0
 1 * 0.5 + 0.5 = 1

Recall that clip space components range from negative one to one and that UV coordinates range from zero to one. To transform clip space coordinates to UV coordinates, multiply by one half and add one half.

    // ...

    vec4 offsetPosition = texture(positionTexture, offsetUV.xy);

    float occluded = 0;
    if (samplePosition.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; }

    // ...

Using the offset UV coordinates, created by projecting the 3D sample onto the 2D position texture, find the corresponding position vector. This takes you from view space to clip space to UV space back to view space. The shader takes this round trip to find out if some geometry is behind, at, or in front of this sample. If the sample is in front of or at some geometry, this sample doesn't count towards the fragment being occluded. If the sample is behind some geometry, this sample counts towards the fragment being occluded.

    // ...

    float intensity =
      smoothstep
        ( 0.0
        , 1.0
        ,   radius
          / abs(position.y - offsetPosition.y)
        );
    occluded *= intensity;

    occlusion -= occluded;

    // ...

Now weight this sampled position by how far it is inside or outside the radius. Finally, subtract this sample from the occlusion factor since it assumes all of the samples are occluded before the loop.

    // ...

    occlusion /= NUM_SAMPLES;

    // ...

    fragColor = vec4(vec3(occlusion), position.a);

    // ...

Divide the occluded count by the number of samples to scale the occlusion factor from [0, NUM_SAMPLES] to [0, 1]. Zero means full occlusion and one means no occlusion. Now assign the occlusion factor to the fragment's color and you're done.

    // ...

    fragColor = vec4(vec3(occlusion), position.a);

    // ...

For the demo's purposes, the example code sets the alpha channel to alpha channel of the position framebuffer texture to avoid covering up the background.

Blurring

SSAO Blur Texture

The SSAO framebuffer texture is noisy as is. You'll want to blur it to remove the noise. Refer back to the section on blurring. For the best results, use a median or Kuwahara filter to preserve the sharp edges.

Ambient Color

  // ...

  vec2 ssaoBlurTexSize  = textureSize(ssaoBlurTexture, 0).xy;
  vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize;
  float ssao            = texture(ssaoBlurTexture, ssaoBlurTexCoord).r;

  vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex * ssao;

  // ...

The final stop for SSAO is back in the lighting calculation. Here you see the occlusion factor being looked up in the SSAO framebuffer texture and then included in the ambient light calculation.

Source

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: docs/style.css ================================================ html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } body { color: #444; font-family: Helvetica, Arial, sans-serif; font-size: 20px; line-height: 2; padding: 1em; margin: auto; max-width: 887px; background: #fefefe; } a { color: #059; text-decoration: none; } a:visited { color: #048; } a:hover { color: #06a; } a:active { color: #06a; } a:focus { outline: thin dotted; } *::-moz-selection { background: rgba(0, 200, 255, 0.3); color: #111; } *::selection { background: rgba(0, 200, 255, 0.3); color: #111; } a::-moz-selection { background: rgba(0, 200, 255, 0.3); color: #048; } a::selection { background: rgba(0, 200, 255, 0.3); color: #048; } a > span.emoji { font-size: 30px; margin-left: 5px; } p { margin: 1em 0; } img { max-width: 100%; } h1, h2, h3, h4, h5, h6 { color: #111; line-height: 125%; margin-top: 1em; font-weight: lighter; font-family: 'Roboto Condensed', Helvetica, Arial, sans-serif; } h4, h5, h6 { font-weight: bold; } h1 { font-size: 2.5em; } h2 { font-size: 2em; } h3 { font-size: 1.5em; } h4 { font-size: 1.2em; } h5 { font-size: 1em; } h6 { font-size: 0.9em; } blockquote { color: #666; text-align: justify; margin-top: 60px; } blockquote > footer { text-align: right; } blockquote::before { content: open-quote; font-size: 100px; color: #999; line-height: 0px; padding-right: 5px; vertical-align: text-bottom; } hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa; border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; } pre, code, kbd, samp { font-family: monospace; font-size: 14px; } pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; } b, strong { font-weight: bold; } p > code { font-weight: bold; color: #444; background-color: #2326291c; padding: 0px 2px 0px 2px; } dfn { font-style: italic; } ins { background: #ff9; color: #000; text-decoration: none; } mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } ul, ol { margin: 1em 0; padding: 0 0 0 2em; } li p:last-child { margin-bottom: 0; } ul ul, ol ol { margin: .3em 0; } dl { margin-bottom: 1em; } dt { font-weight: bold; margin-bottom: .8em; } dd { margin: 0 0 .8em 2em; } dd:last-child { margin-bottom: 0; } img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } figure { display: block; text-align: center; margin: 1em 0; } figure img { border: none; margin: 0 auto; } figcaption { font-size: 0.8em; font-style: italic; margin: 0 0 .8em; } table { margin-bottom: 2em; border-bottom: 1px solid #ddd; border-right: 1px solid #ddd; border-spacing: 0; border-collapse: collapse; } table th { padding: .2em 1em; background-color: #eee; border-top: 1px solid #ddd; border-left: 1px solid #ddd; } table td { padding: .2em 1em; border-top: 1px solid #ddd; border-left: 1px solid #ddd; vertical-align: top; } kbd { border: 1px solid #999; padding: 5px; border-radius: 2px; background-color: #555; color: #eee; white-space: nowrap; } .author { font-size: 1.2em; text-align: center; } @media print { * { background: transparent !important; color: black !important; filter: none !important; -ms-filter: none !important; } body { font-size: 12pt; max-width: 100%; } a, a:visited { text-decoration: underline; } hr { height: 1px; border: 0; border-bottom: 1px solid black; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } pre, blockquote { border: 1px solid #999; padding-right: 1em; page-break-inside: avoid; } tr, img { page-break-inside: avoid; } img { max-width: 100% !important; } @page :left { margin: 15mm 20mm 15mm 10mm; } @page :right { margin: 15mm 10mm 15mm 20mm; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } } ================================================ FILE: docs/texturing.html ================================================ Texturing | 3D Game Shaders For Beginners

◀️ 🔼 🔽 ▶️

3D Game Shaders For Beginners

Texturing

Diffuse Texture Only

Texturing involves mapping some color or some other kind of vector to a fragment using UV coordinates. Both U and V range from zero to one. Each vertex gets a UV coordinate and this is outputted in the vertex shader.

UV Interpolation

The fragment shader receives the UV coordinate interpolated. Interpolated meaning the UV coordinate for the fragment is somewhere between the UV coordinates for the vertexes that make up the triangle face.

Vertex

#version 150

uniform mat4 p3d_ModelViewProjectionMatrix;

in vec2 p3d_MultiTexCoord0;

in vec4 p3d_Vertex;

out vec2 texCoord;

void main()
{
  texCoord = p3d_MultiTexCoord0;

  gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}

Here you see the vertex shader outputting the texture coordinate to the fragment shader. Notice how it's a two dimensional vector. One dimension for U and one for V.

Fragment

#version 150

uniform sampler2D p3d_Texture0;

in vec2 texCoord;

out vec2 fragColor;

void main()
{
  texColor = texture(p3d_Texture0, texCoord);

  fragColor = texColor;
}

Here you see the fragment shader looking up the color at its UV coordinate and outputting that as the fragment color.

Screen Filled Texture

#version 150

uniform sampler2D screenSizedTexture;

out vec2 fragColor;

void main()
{
  vec2 texSize  = textureSize(texture, 0).xy;
  vec2 texCoord = gl_FragCoord.xy / texSize;

  texColor = texture(screenSizedTexture, texCoord);

  fragColor = texColor;
}

When performing render to texture, the mesh is a flat rectangle with the same aspect ratio as the screen. Because of this, you can calculate the UV coordinates knowing only A) the width and height of the screen sized texture being UV mapped to the rectangle and B) the fragment's x and y coordinate. To map x to U, divide x by the width of the input texture. Similarly, to map y to V, divide y by the height of the input texture. You'll see this technique used in the example code.

(C) 2019 David Lettier
lettier.com

◀️ 🔼 🔽 ▶️

================================================ FILE: sections/blinn-phong.md ================================================ [:arrow_backward:](lighting.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](fresnel-factor.md) # 3D Game Shaders For Beginners ## Blinn-Phong

Blinn-Phong

Blinn-Phong is a slight adjustment of the Phong model you saw in the [lighting](lighting.md) section. It provides more plausible or realistic specular reflections. You'll notice that Blinn-Phong produces elliptical or elongated specular reflections versus the spherical specular reflections produced by the Phong model. In certain cases, Blinn-Phong can be more efficient to calculate than Phong. ```c // ... vec3 light = normal(lightPosition.xyz - vertexPosition.xyz); vec3 eye = normalize(-vertexPosition.xyz); vec3 halfway = normalize(light + eye); // ... ``` Instead of computing the reflection vector, compute the halfway or half angle vector. This vector is between the view/camera/eye and light direction vector.

Blinn-Phong vs Phong

```c // ... float specularIntensity = dot(normal, halfway); // ... ``` The specular intensity is now the dot product of the normal and halfway vector. In the Phong model, it is the dot product of the reflection and view vector.

Full specular intensity.

The half angle vector (magenta arrow) will point in the same direction as the normal (green arrow) when the view vector (orange arrow) points in the same direction as the reflection vector (magenta arrow). In this case, both the Blinn-Phong and Phong specular intensity will be one.

Blinn-Phong vs Phong

In other cases, the specular intensity for Blinn-Phong will be greater than zero while the specular intensity for Phong will be zero. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](lighting.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](fresnel-factor.md) ================================================ FILE: sections/bloom.md ================================================ [:arrow_backward:](blur.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](ssao.md) # 3D Game Shaders For Beginners ## Bloom

Bloom

Adding bloom to a scene can really sell the illusion of the lighting model. Light emitting objects are more believable and specular highlights get an extra dose of shimmer. ```c //... int size = 5; float separation = 3; float threshold = 0.4; float amount = 1; // ... ``` These parameters control the look and feel. `size` determines how blurred the effect is. `separation` spreads out the blur. `threshold` controls which fragments are illuminated. And the last parameter, `amount`, controls how much bloom is outputted. ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; float value = 0.0; float count = 0.0; vec4 result = vec4(0); vec4 color = vec4(0); for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { // ... } } // ... ``` The technique starts by looping through a kernel/matrix/window centered over the current fragment. This is similar to the window used for [outlining](outlining.md). The size of the window is `size * 2 + 1` by `size * 2 + 1`. So for example, with a `size` setting of two, the window uses `(2 * 2 + 1)^2 = 25` samples per fragment. ```c // ... color = texture ( colorTexture , ( gl_FragCoord.xy + (vec2(i, j) * separation) ) / texSize ); value = max(color.r, max(color.g, color.b)); if (value < threshold) { color = vec4(0); } result += color; count += 1.0; // ... ``` For each iteration, it retrieves the color from the input texture and turns the red, green, and blue values into a greyscale value. If this greyscale value is less than the threshold, it discards this color by making it solid black. After evaluating the sample's greyscale value, it adds its RGB values to `result`. ```c // ... result /= count; fragColor = mix(vec4(0), result, amount); // ... ``` After it's done summing up the samples, it divides the sum of the color samples by the number of samples taken. The result is the average color of itself and its neighbors. By doing this for every fragment, you end up with a blurred image. This form of blurring is known as a [box blur](blur.md#box-blur).

Bloom progresssion.

Here you see the progression of the bloom algorithm. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [bloom.frag](../demonstration/shaders/fragment/outline.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](blur.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](ssao.md) ================================================ FILE: sections/blur.md ================================================ [:arrow_backward:](fog.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](bloom.md) # 3D Game Shaders For Beginners ## Blur

Kuwahara Filter

The need to blur this or that can come up quite often as you try to obtain a particular look or perform some technique like motion blur. Below are just some of ways you can blur your game's imagery. ### Box Blur

Box Blur

The box blur or mean filter algorithm is a simple to implement blurring effect. It's fast and gets the job done. If you need more finesse, you can upgrade to a Gaussian blur. ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; int size = int(parameters.x); if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; } // ... ``` The `size` parameter controls how blurry the result is. If the `size` is zero or less, return the fragment untouched. ```c // ... float separation = parameters.y; separation = max(separation, 1); // ... ``` The `separation` parameter spreads out the blur without having to sample additional fragments. `separation` ranges from one to infinity.

Blur Kernel

```c // ... for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { // ... } } // ... ``` Like the [outlining](outlining.md) technique, the box blur technique uses a kernel/matrix/window centered around the current fragment. The size of the window is `size * 2 + 1` by `size * 2 + 1`. So for example, with a `size` setting of two, the window uses `(2 * 2 + 1)^2 = 25` samples per fragment. ```c // ... fragColor += texture ( colorTexture , ( gl_FragCoord.xy + (vec2(i, j) * separation) ) / texSize ); // ... ``` To compute the mean or average of the samples in the window, start by loop through the window, adding up each color vector. ```c // ... fragColor /= pow(size * 2 + 1, 2); // ... ``` To finish computing the mean, divide the sum of the colors sampled by the number of samples taken. The final fragment color is the mean or average of the fragments sampled inside the window. ### Median Filter

Median Filter

The box blur uses the mean color of the samples taken. The median filter uses the median color of the samples taken. By using the median instead of the mean, the edges in the image are preserved—meaning the edges stay nice and crisp. For example, look at the windows in the box blurred image versus the median filtered image. Unfortunately, finding the median can be slower than finding the mean. You could sort the values and choose the middle one but that would take at least quasilinear time. There is a technique to find the median in linear time but it can be quite awkward inside a shader. The numerical approach below approximates the median in linear time. How well it approximates the median can be controlled.

Painterly

At lower quality approximations, you end up with a nice [painterly](https://en.wikipedia.org/wiki/Painterliness) look. ```c // ... #define MAX_SIZE 4 #define MAX_KERNEL_SIZE ((MAX_SIZE * 2 + 1) * (MAX_SIZE * 2 + 1)) #define MAX_BINS_SIZE 100 // ... ``` These are the hard limits for the `size` parameter, window size, and `bins` array. ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; int size = int(parameters.x); if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; } if (size > MAX_SIZE) { size = MAX_SIZE; } int kernelSize = int(pow(size * 2 + 1, 2)); // ... ``` The `size` parameter controls how blurry or smeared the effect is. If the size is at or below zero, return the current fragment untouched. From the `size` parameter, calculate the total size of the kernel or window. This is how many samples you'll be taking per fragment. ```c // ... int binsSize = int(parameters.y); binsSize = clamp(binsSize, 1, MAX_BINS_SIZE); // ... ``` Set up the `binsSize`, making sure to limit it by the `MAX_BINS_SIZE`. ```c // ... int i = 0; int j = 0; int count = 0; int binIndex = 0; // ... ``` `i` and `j` are used to sample the given texture around the current fragment. `i` is also used as a general for loop count. `count` is used in the initialization of the `colors` array which you'll see later. `binIndex` is used to approximate the median color. ```c // ... vec4 colors[MAX_KERNEL_SIZE]; float bins[MAX_BINS_SIZE]; int binIndexes[colors.length()]; // ... ``` The `colors` array holds the sampled colors taken from the input texture. `bins` is used to approximate the median of the sampled colors. Each bin holds a count of how many colors fall into its range when converting each color into a greyscale value (between zero and one). As `binsSize` approaches 100, the algorithm finds the true median almost always. `binIndexes` stores the `bins` index or which bin each sample falls into. ```c // ... float total = 0; float limit = floor(float(kernelSize) / 2) + 1; // ... ``` `total` keeps track of how many colors you've come across as you loop through `bins`. When `total` reaches `limit`, you return whatever `bins` index you're at. The `limit` is the median index. For example, if the window size is 81, `limit` is 41 which is directly in the middle (40 samples below and 40 samples above). ```c // ... float value = 0; vec3 valueRatios = vec3(0.3, 0.59, 0.11); // ... ``` These are used to covert and hold each color sample's greyscale value. Instead of dividing red, green, and blue by one third, it uses 30% of red, 59% of green, and 11% of blue for a total of 100%. ```c // ... for (i = -size; i <= size; ++i) { for (j = -size; j <= size; ++j) { colors[count] = texture ( colorTexture , ( gl_FragCoord.xy + vec2(i, j) ) / texSize ); count += 1; } } // ... ``` Loop through the window and collect the color samples into `colors`. ```c // ... for (i = 0; i < binsSize; ++i) { bins[i] = 0; } // ... ``` Initialize the `bins` array with zeros. ```c // ... for (i = 0; i < kernelSize; ++i) { value = dot(colors[i].rgb, valueRatios); binIndex = int(floor(value * binsSize)); binIndex = clamp(binIndex, 0, binsSize - 1); bins[binIndex] += 1; binIndexes[i] = binIndex; } // ... ``` Loop through the colors and convert each one to a greyscale value. `dot(colors[i].rgb, valueRatios)` is the weighted sum `colors.r * 0.3 + colors.g * 0.59 + colors.b * 0.11`. Each value will fall into some bin. Each bin covers some range of values. For example, if the number of bins is 10, the first bin covers everything from zero up to but not including 0.1. Increment the number of colors that fall into this bin and remember the color sample's bin index so you can look it up later. ```c // ... binIndex = 0; for (i = 0; i < binsSize; ++i) { total += bins[i]; if (total >= limit) { binIndex = i; break; } } // ... ``` Loop through the bins, tallying up the number of colors seen so far. When you reach the median index, exit the loop and remember the last `bins` index reached. ```c // ... fragColor = colors[0]; for (i = 0; i < kernelSize; ++i) { if (binIndexes[i] == binIndex) { fragColor = colors[i]; break; } } // ... ``` Now loop through the `binIndexes` and find the first color with the last `bins` indexed reached. Its greyscale value is the approximated median which in many cases will be the true median value. Set this color as the fragColor and exit the loop and shader. ### Kuwahara Filter

Kuwahara Filter

Like the median filter, the kuwahara filter preserves the major edges found in the image. You'll notice that it has a more block like or chunky pattern to it. In practice, the Kuwahara filter runs faster than the median filter, allowing for larger `size` values without a noticeable slowdown. ```c // ... #define MAX_SIZE 5 #define MAX_KERNEL_SIZE ((MAX_SIZE * 2 + 1) * (MAX_SIZE * 2 + 1)) // ... ``` Set a hard limit for the `size` parameter and the number of samples taken. ```c // ... int i = 0; int j = 0; int count = 0; // ... ``` These are used to sample the input texture and set up the `values` array. ```c // ... vec3 valueRatios = vec3(0.3, 0.59, 0.11); // ... ``` Like the median filter, you'll be converting the color samples into greyscale values. ```c // ... float values[MAX_KERNEL_SIZE]; // ... ``` Initialize the `values` array. This will hold the greyscale values for the color samples. ```c // ... vec4 color = vec4(0); vec4 meanTemp = vec4(0); vec4 mean = vec4(0); float valueMean = 0; float variance = 0; float minVariance = -1; // ... ``` The Kuwahara filter works by computing the variance of four subwindows and then using the mean of the subwindow with the smallest variance. ```c // ... void findMean(int i0, int i1, int j0, int j1) { // ... ``` `findMean` is a function defined outside of `main`. Each run of `findMean` will remember the mean of the given subwindow that has the lowest variance seen so far. ```c // ... meanTemp = vec4(0); count = 0; // ... ``` Make sure to reset `count` and `meanTemp` before computing the mean of the given subwindow. ```c // ... for (i = i0; i <= i1; ++i) { for (j = j0; j <= j1; ++j) { color = texture ( colorTexture , (gl_FragCoord.xy + vec2(i, j)) / texSize ); meanTemp += color; values[count] = dot(color.rgb, valueRatios); count += 1; } } // ... ``` Similar to the box blur, loop through the given subwindow and add up each color. At the same time, make sure to store the greyscale value for this sample in `values`. ```c // ... meanTemp.rgb /= count; valueMean = dot(meanTemp.rgb, valueRatios); // ... ``` To compute the mean, divide the samples sum by the number of samples taken. Calculate the greyscale value for the mean. ```c // ... for (i = 0; i < count; ++i) { variance += pow(values[i] - valueMean, 2); } variance /= count; // ... ``` Now calculate the variance for this given subwindow. The variance is the average squared difference between each sample's greyscale value the mean greyscale value. ```c // ... if (variance < minVariance || minVariance <= -1) { mean = meanTemp; minVariance = variance; } } // ... ``` If the variance is smaller than what you've seen before or this is the first variance you've seen, set the mean of this subwindow as the final mean and update the minimum variance seen so far. ```c // ... void main() { int size = int(parameters.x); if (size <= 0) { fragColor = texture(colorTexture, texCoord); return; } // ... ``` Back in `main`, set the `size` parameter. If the size is at or below zero, return the fragment unchanged.

Kuwahara Kernal

```c // ... // Lower Left findMean(-size, 0, -size, 0); // Upper Right findMean(0, size, 0, size); // Upper Left findMean(-size, 0, 0, size); // Lower Right findMean(0, size, -size, 0); // ... ``` As stated above, the Kuwahara filter works by computing the variance of four subwindows and then using the mean of the subwindow with the lowest variance as the final fragment color. Note that the four subwindows overlap each other. ```c // ... mean.a = 1; fragColor = mean; // ... ``` After computing the variance and mean for each subwindow, set the fragment color to the mean of the subwindow with the lowest variance. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [box-blur.frag](../demonstration/shaders/fragment/box-blur.frag) - [median-filter.frag](../demonstration/shaders/fragment/median-filter.frag) - [kuwahara-filter.frag](../demonstration/shaders/fragment/kuwahara-filter.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](fog.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](bloom.md) ================================================ FILE: sections/building-the-demo.md ================================================ [:arrow_backward:](setup.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](running-the-demo.md) # 3D Game Shaders For Beginners ## Building The Demo

Building The Demo

Before you can try out the demo program, you'll have to build the example code first. ### Dependencies Before you can compile the example code, you'll need to install [Panda3D](https://www.panda3d.org/) for your platform. Panda3D is available for Linux, Mac, and Windows. ### Linux Start by [installing](https://www.panda3d.org/manual/?title=Installing_Panda3D_in_Linux) the [Panda3D SDK](https://www.panda3d.org/download/sdk-1-10-9/) for your distribution. Make sure to locate where the Panda3D headers and libraries are. The headers and libraries are most likely in `/usr/include/panda3d/` and `/usr/lib/panda3d/` respectively. Next clone this repository and change directory into it. ```bash git clone https://github.com/lettier/3d-game-shaders-for-beginners.git cd 3d-game-shaders-for-beginners/demonstration ``` Now compile the source code into an object file. ```bash g++ \ -c src/main.cxx \ -o 3d-game-shaders-for-beginners.o \ -std=gnu++11 \ -O2 \ -I/path/to/python/include/ \ -I/path/to/panda3d/include/ ``` With the object file created, create the executable by linking the object file to its dependencies. ```bash g++ \ 3d-game-shaders-for-beginners.o \ -o 3d-game-shaders-for-beginners \ -L/path/to/panda3d/lib \ -lp3framework \ -lpanda \ -lpandafx \ -lpandaexpress \ -lpandaphysics \ -lp3dtoolconfig \ -lp3dtool \ -lpthread ``` For more help, see the [Panda3D manual](https://www.panda3d.org/manual/?title=How_to_compile_a_C++_Panda3D_program_on_Linux). ### Mac Start by installing the [Panda3D SDK](https://www.panda3d.org/download/sdk-1-10-9/) for Mac. Make sure to locate where the Panda3D headers and libraries are. Next clone this repository and change directory into it. ```bash git clone https://github.com/lettier/3d-game-shaders-for-beginners.git cd 3d-game-shaders-for-beginners ``` Now compile the source code into an object file. You'll have to find where the Python 2.7 and Panda3D include directories are. ```bash clang++ \ -c main.cxx \ -o 3d-game-shaders-for-beginners.o \ -std=gnu++11 \ -g \ -O2 \ -I/path/to/python/include/ \ -I/path/to/panda3d/include/ ``` With the object file created, create the executable by linking the object file to its dependencies. You'll need to track down where the Panda3D libraries are located. ```bash clang++ \ 3d-game-shaders-for-beginners.o \ -o 3d-game-shaders-for-beginners \ -L/path/to/panda3d/lib \ -lp3framework \ -lpanda \ -lpandafx \ -lpandaexpress \ -lpandaphysics \ -lp3dtoolconfig \ -lp3dtool \ -lpthread ``` For more help, see the [Panda3D manual](https://www.panda3d.org/manual/?title=How_to_compile_a_C++_Panda3D_program_on_macOS). ### Windows Start by [installing](https://www.panda3d.org/manual/?title=Installing_Panda3D_in_Windows) the [Panda3D SDK](https://www.panda3d.org/download/sdk-1-10-9/) for Windows. Make sure to locate where the Panda3D headers and libraries are. Next clone this repository and change directory into it. ```bash git clone https://github.com/lettier/3d-game-shaders-for-beginners.git cd 3d-game-shaders-for-beginners ``` For more help, see the [Panda3D manual](https://www.panda3d.org/manual/?title=Running_your_Program&language=cxx). ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](setup.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](running-the-demo.md) ================================================ FILE: sections/cel-shading.md ================================================ [:arrow_backward:](rim-lighting.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](normal-mapping.md) # 3D Game Shaders For Beginners ## Cel Shading

Cel Shaded

Cel shading is a technique to make 3D objects look 2D or flat. In 2D, you can make an object look 3D by applying a smooth gradient. However, with cel shading, you're breaking up the gradients into abrupt transitions. Typically there is only one transition where the shading goes from fully lit to fully shadowed. When combined with [outlining](outlining.md), cel shading can really sell the 2D cartoon look. ## Diffuse ```c // ... float diffuseIntensity = max(dot(normal, unitLightDirection), 0.0); diffuseIntensity = step(0.1, diffuseIntensity); // ... ``` Revisiting the [lighting](lighting.md#diffuse) model, modify the `diffuseIntensity` such that it is either zero or one.

Step Function

The `step` function returns zero if the input is less than the edge and one otherwise.

Steps Function

```c // ... if (diffuseIntensity >= 0.8) { diffuseIntensity = 1.0; } else if (diffuseIntensity >= 0.6) { diffuseIntensity = 0.6; } else if (diffuseIntensity >= 0.3) { diffuseIntensity = 0.3; } else { diffuseIntensity = 0.0; } // ... ``` If you would like to have a few steps or transitions, you can perform something like the above.

Step Texture

```c // ... diffuseIntensity = texture(steps, vec2(diffuseIntensity, 0.0)).r; // ... ``` Another approach is to put your step values into a texture with the transitions going from darker to lighter. Using the `diffuseIntensity` as a U coordinate, it will automatically transform itself. ## Specular ```c // ... float specularIntensity = clamp(dot(normal, halfwayDirection), 0.0, 1.0); specularIntensity = step(0.98, specularIntensity); // ... ``` Using the `step` function again, set the `specularIntensity` to be either zero or one. You can also use one of the other approaches described up above for the specular highlight as well. After you've altered the `specularIntensity`, the rest of the lighting calculations are the same. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](rim-lighting.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](normal-mapping.md) ================================================ FILE: sections/chromatic-aberration.md ================================================ [:arrow_backward:](motion-blur.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](screen-space-reflection.md) # 3D Game Shaders For Beginners ## Chromatic Aberration

Chromatic Aberration

Chromatic aberration is a screen space technique that simulates lens distortion. Use it to give your scene a cinematic, lo-fi analog feel or to emphasize a chaotic event. ### Texture ```c uniform sampler2D colorTexture; // ... ``` The input texture needed is the scene's colors captured into a framebuffer texture. ### Parameters

Chromatic Aberration

```c // ... float redOffset = 0.009; float greenOffset = 0.006; float blueOffset = -0.006; // ... ``` The adjustable parameters for this technique are the red, green, and blue offsets. Feel free to play around with these to get the particular color fringe you're looking for. These particular offsets produce a yellowish orange and blue fringe. ### Direction ```c // ... uniform vec2 mouseFocusPoint; // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec2 direction = texCoord - mouseFocusPoint; // ... ``` The offsets can occur horizontally, vertically, or radially. One approach is to radiate out from the [depth of field](depth-of-field.md) focal point. As the scene gets more out of focus, the chromatic aberration increases. ### Samples ```c // ... out vec4 fragColor; // ... fragColor.r = texture(colorTexture, texCoord + (direction * vec2(redOffset ))).r; fragColor.g = texture(colorTexture, texCoord + (direction * vec2(greenOffset))).g; fragColor.ba = texture(colorTexture, texCoord + (direction * vec2(blueOffset ))).ba; } ``` With the direction and offsets, make three samples of the scene's colors—one for the red, green, and blue channels. These will be the final fragment color. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [chromatic-aberration.frag](../demonstration/shaders/fragment/chromatic-aberration.frag) ## Copyright (C) 2021 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](motion-blur.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](screen-space-reflection.md) ================================================ FILE: sections/deferred-rendering.md ================================================ [:arrow_backward:](normal-mapping.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](fog.md) # 3D Game Shaders For Beginners ## Deferred Rendering

Deferred vs Forward

Deferred rendering (deferred shading) is a screen space lighting technique. Instead of calculating the lighting for a scene while you traverse its geometry—you defer or wait to perform the lighting calculations until after the scene's geometry fragments have been culled or discarded. This can give you a performance boost depending on the complexity of your scene. ### Phases

G-buffer

Deferred rendering is performed in two phases. The first phase involves going through the scene's geometry and rendering its positions or depths, normals, and materials into a framebuffer known as the geometry buffer or G-buffer. With the exception of some transformations, this is mostly a read-only phase so its performance cost is minimal. After this phase, you're only dealing with 2D textures in the shape of the screen.

Lighting Phase

The second and last phase is where you perform your lighting calculations using the output of the first phase. This is when you calculate the ambient, diffuse, and specular colors. Shadow and normal mapping are performed in this phase as well. ### Advantages The reason for using deferred rendering is to reduce the number of lighting calculations made. With forward rendering, the number of lighting calculations scales with the number of fragments and lights. However, with deferred shading, the number of lighting calculations scales with the number of pixels and lights. Recall that for a single pixel, there can be multiple fragments produced. As you add geometry, the number of lighting calculations per pixel increases when using forward but not when using deferred. For simple scenes, deferred rendering doesn't provide much of a performance boost and may even hurt performance. However, for complex scenes with lots of lighting, it becomes the better option. Deferred becomes faster than forward because you're only calculating the lighting per light, per pixel. In forward rendering, you're calculating the lighting per light per fragment which can be multiple times per pixel. ### Disadvantages Deferred rendering allows you render complex scenes using many lights but it does come with its own set of tradeoffs. Transparency becomes an issue because the geometry data you could see through a semitransparent object is discarded in the first phase. Other tradesoffs include increased memory consumption due to the G-buffer and the extra workarounds needed to deal with aliasing. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](normal-mapping.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](fog.md) ================================================ FILE: sections/depth-of-field.md ================================================ [:arrow_backward:](outlining.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](posterization.md) # 3D Game Shaders For Beginners ## Depth Of Field

Depth Of Field

Like [SSAO](ssao.md), depth of field is an effect you can't live without once you've used it. Artistically, you can use it to draw your viewer's eye to a certain subject. But in general, depth of field adds a lot of realism for a little bit of effort. ### In Focus The first step is to render your scene completely in focus. Be sure to render this into a framebuffer texture. This will be one of the inputs to the depth of field shader. ### Out Of Focus The second step is to blur the scene as if it was completely out of focus. Like bloom and SSAO, you can use a [box blur](blur.md#box-blur). Be sure to render this out-of-focus-scene into a framebuffer texture. This will be one of the inputs to the depth of field shader. #### Bokeh

Bokeh

For a great bokeh effect, dilate the out of focus texture and use that as the out of focus input. See [dilation](dilation.md) for more details. ### Mixing ```c // ... float minDistance = 1.0; float maxDistance = 3.0; // ... ``` Feel free to tweak these two parameters. All positions at or below `minDistance` will be completely in focus. All positions at or beyond `maxDistance` will be completely out of focus. ```c // ... vec4 focusColor = texture(focusTexture, texCoord); vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord); // ... ``` You'll need the in focus and out of focus colors. ```c // ... vec4 position = texture(positionTexture, texCoord); // ... ``` You'll also need the vertex position in view space. You can reuse the position framebuffer texture you used for [SSAO](ssao.md#vertex-positions). ```c // ... vec4 focusPoint = texture(positionTexture, mouseFocusPoint); // ... ``` The focus point is a position somewhere in your scene. All of the points in your scene are measured from the focus point. Choosing the focus point is up to you. The demo uses the scene position directly under the mouse when clicking the middle mouse button. However, it could be a constant distance from the camera or a static position.

smoothstep

```c // ... float blur = smoothstep ( minDistance , maxDistance , abs(position.y - focusPoint.y) ); // ... ``` `smoothstep` returns values from zero to one. The `minDistance` is the left-most edge. Any position less than the minimum distance, from the focus point, will be in focus or have a blur of zero. The `maxDistance` is the right-most edge. Any position greater than the maximum distance, from the focus point, will be out of focus or have a blur or one. For distances between the edges, blur will be between zero and one. These values are interpolated along a s-shaped curve. ```c // ... fragColor = mix(focusColor, outOfFocusColor, blur); // ... ``` The `fragColor` is a mixture of the in focus and out of focus color. The closer `blur` is to one, the more it will use the `outOfFocusColor`. Zero `blur` means this fragment is entirely in focus. At `blur >= 1`, this fragment is completely out of focus. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [box-blur.frag](../demonstration/shaders/fragment/box-blur.frag) - [depth-of-field.frag](../demonstration/shaders/fragment/depth-of-field.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](outlining.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](posterization.md) ================================================ FILE: sections/dilation.md ================================================ [:arrow_backward:](sharpen.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](film-grain.md) # 3D Game Shaders For Beginners ## Dilation

Dilation

Dilation dilates or enlarges the brighter areas of an image while at the same time, contracts or shrinks the darker areas of an image. This tends to create a pillowy look. You can use dilation for a glow/bloom effect or to add bokeh to your [depth of field](depth-of-field.md). ```c // ... int size = int(parameters.x); float separation = parameters.y; float minThreshold = 0.1; float maxThreshold = 0.3; // ... ``` The `size` and `separation` parameters control how dilated the image becomes. A larger `size` will increase the dilation at the cost of performance. A larger `separation` will increase the dilation at the cost of quality. The `minThreshold` and `maxThreshold` parameters control which parts of the image become dilated. ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 fragCoord = gl_FragCoord.xy; fragColor = texture(colorTexture, fragCoord / texSize); // ... ``` Sample the color at the current fragment's position. ```c // ... float mx = 0.0; vec4 cmx = fragColor; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { // ... } } // ... ``` Loop through a `size` by `size` window, centered at the current fragment position. As you loop, find the brightest color based on the surrounding greyscale values.

Dilation Window

```c // ... // For a rectangular shape. //if (false); // For a diamond shape; //if (!(abs(i) <= size - abs(j))) { continue; } // For a circular shape. if (!(distance(vec2(i, j), vec2(0, 0)) <= size)) { continue; } // ... ``` The window shape will determine the shape of the dilated parts of the image. For a rectangular shape, you can use every fragment covered by the window. For any other shape, skip the fragments that fall outside the desired shape. ```c // ... vec4 c = texture ( colorTexture , ( gl_FragCoord.xy + (vec2(i, j) * separation) ) / texSize ); // ... ``` Sample a fragment color from the surrounding window. ```c // ... float mxt = dot(c.rgb, vec3(0.21, 0.72, 0.07)); // ... ``` Convert the sampled color to a greyscale value. ```c // ... if (mxt > mx) { mx = mxt; cmx = c; } // ... ``` If the sampled greyscale value is larger than the current maximum greyscale value, update the maximum greyscale value and its corresponding color. ```c // ... fragColor.rgb = mix ( fragColor.rgb , cmx.rgb , smoothstep(minThreshold, maxThreshold, mx) ); // ... ``` The new fragment color is a mixture between the existing fragment color and the brightest color found. If the maximum greyscale value found is less than `minThreshold`, the fragment color is unchanged. If the maximum greyscale value is greater than `maxThreshold`, the fragment color is replaced with the brightest color found. For any other case, the fragment color is a mix between the current fragment color and the brightest color. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [dilation.frag](../demonstration/shaders/fragment/dilation.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](sharpen.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](film-grain.md) ================================================ FILE: sections/film-grain.md ================================================ [:arrow_backward:](dilation.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](lookup-table.md) # 3D Game Shaders For Beginners ## Film Grain

Film Grain

Film grain (when applied in subtle doses, unlike here) can add a bit of realism you don't notice until it's removed. Typically, it's the imperfections that make a digitally generated image more believable. In terms of the shader graph, film grain is usually the last effect applied before the game is put on screen. ### Amount ```c // ... float amount = 0.1; // ... ``` The `amount` controls how noticeable the film grain is. Crank it up for a snowy picture. ### Random Intensity ```c // ... uniform float osg_FrameTime; //... float toRadians = 3.14 / 180; //... float randomIntensity = fract ( 10000 * sin ( ( gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime ) * toRadians ) ); // ... ``` This snippet calculates the random intensity needed to adjust the amount. ```c Time Since F1 = 00 01 02 03 04 05 06 07 08 09 10 Frame Number = F1 F3 F4 F5 F6 osg_FrameTime = 00 02 04 07 08 ``` `osg_FrameTime` is [provided](https://github.com/panda3d/panda3d/blob/daa57733cb9b4ccdb23e28153585e8e20b5ccdb5/panda/src/display/graphicsStateGuardian.cxx#L930) by Panda3D. The frame time is a timestamp of how many seconds have passed since the first frame. The example code uses this to animate the film grain as `osg_FrameTime` will always be different each frame. ```c // ... ( gl_FragCoord.x + gl_FragCoord.y * 8009 // Large number here. // ... ``` For static film grain, replace `osg_FrameTime` with a large number. You may have to try different numbers to avoid seeing any patterns.

Horizontal, vertical, and diagonal lines.

```c // ... * sin ( ( gl_FragCoord.x + gl_FragCoord.y * someNumber // ... ``` Both the x and y coordinate are used to create points or specs of film grain. If only x was used, there would only be vertical lines. Similarly, if only y was used, there would be only horizontal lines. The reason the snippet multiplies one coordinate by some number is to break up the diagonal symmetry.

Rain

You can of course remove the coordinate multiplier for a somewhat decent looking rain effect. To animate the rain effect, multiply the output of `sin` by `osg_FrameTime`. ```c // ... ( gl_FragCoord.x + gl_FragCoord.y // ... ``` Play around with the x and y coordinate to try and get the rain to change directions. Keep only the x coordinate for a straight downpour. ```c input = (gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime) * toRadians frame(10000 * sin(input)) = fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = ``` `sin` is used as a hashing function. The fragment's coordinates are hashed to some output of `sin`. This has the nice property that no matter the input (big or small), the output range is negative one to one. ```c fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = fract(6400.723818964882) = 0.723818964882 ``` `sin` is also used as a pseudo random number generator when combined with `fract`. ```python >>> [floor(fract(4 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 0, 1, 2, 2, 3, 4, 4, 5, 6] >>> [floor(fract(10000 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 4, 8, 0, 2, 1, 7, 0, 0, 5] ``` Take a look at the first sequence of numbers and then the second. Each sequence is deterministic but the second sequence has less of a pattern than the first. So while the output of `fract(10000 * sin(...))` is deterministic, it doesn't have much of a discernible pattern.

Increasing the pseudo randomness.

Here you see the `sin` multiplier going from 1, to 10, to 100, and then to 1000. As you increase the `sin` output multiplier, you get less and less of a pattern. This is the reason the snippet multiplies `sin` by 10,000. ### Fragment Color ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 color = texture(colorTexture, texCoord); // ... ``` Convert the fragment's coordinates to UV coordinates. Using these UV coordinates, look up the texture color for this fragment. ```c // ... amount *= randomIntensity; color.rgb += amount; // ... ``` Adjust the amount by the random intensity and add this to the color. ```c // ... fragColor = color; // ... ``` Set the fragment color and you're done. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [film-grain.frag](../demonstration/shaders/fragment/film-grain.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](dilation.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](lookup-table.md) ================================================ FILE: sections/flow-mapping.md ================================================ [:arrow_backward:](foam.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](outlining.md) # 3D Game Shaders For Beginners ## Flow Mapping

Flow Mapping

Flow mapping is useful when you need to animate some fluid material. Much like diffuse maps map UV coordinates to diffuse colors and normal maps map UV coordinates to normals, flow maps map UV coordinates to 2D translations or flows.

Flow Map

Here you see a flow map that maps UV coordinates to translations in the positive y-axis direction. Flow maps use the red and green channels to store translations in the x and y direction. The red channel is for the x-axis and the green channel is the y-axis. Both range from zero to one which translates to flows that range from `(-1, -1)` to `(1, 1)`. This flow map is all one color consisting of 0.5 red and 0.6 green. ```c [r, g, b] = [r * 2 - 1, g * 2 - 1, b * 2 - 1] = [ x, y, z] ``` Recall how the colors in a normal map are converted to actual normals. There is a similar process for flow maps. ```c // ... uniform sampler2D flowTexture; vec2 flow = texture(flowTexture, uv).xy; flow = (flow - 0.5) * 2; // ... ``` To convert a flow map color to a flow, you minus 0.5 from the channel (red and green) and multiply by two. ```c (r, g) = ( (r - 0.5) * 2 , (g - 0.5) * 2 ) = ( (0.5 - 0.5) * 2 , (0.6 - 0.5) * 2 ) = (x, y) = (0, 0.2) ``` The flow map above maps each UV coordinate to the flow `(0, 0.2)`. This indicates zero movement in the x direction and a movement of 0.2 in the y direction. The flows can be used to translate all sorts of things but they're typically used to offset the UV coordinates of a another texture.

Foam Mask

```c // ... vec2 flow = texture(flowTexture, diffuseCoord).xy; flow = (flow - 0.5) * 2; vec4 foamPattern = texture ( foamPatternTexture , vec2 ( diffuseCoord.x + flow.x * osg_FrameTime , diffuseCoord.y + flow.y * osg_FrameTime ) ); // ... ``` For example, the demo program uses a flow map to animate the water. Here you see the flow map being used to animate the [foam mask](foam.md#mask). This continuously moves the diffuse UV coordinates directly up, giving the foam mask the appearance of moving down stream. ```c // ... ( diffuseCoord.x + flow.x * osg_FrameTime , diffuseCoord.y + flow.y * osg_FrameTime // ... ``` You'll need how many seconds have passed since the first frame in order to animate the UV coordinates in the direction indicated by the flow. `osg_FrameTime` is [provided](https://github.com/panda3d/panda3d/blob/daa57733cb9b4ccdb23e28153585e8e20b5ccdb5/panda/src/display/graphicsStateGuardian.cxx#L930) by Panda3D. It is a timestamp of how many seconds have passed since the first frame. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [normal.frag](../demonstration/shaders/fragment/normal.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](foam.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](outlining.md) ================================================ FILE: sections/foam.md ================================================ [:arrow_backward:](screen-space-refraction.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](flow-mapping.md) # 3D Game Shaders For Beginners ## Foam

Foam

Foam is typically used when simulating some body of water. Anywhere the water's flow is disrupted, you add some foam. The foam isn't much by itself but it can really connect the water with the rest of the scene.

Lava River

But don't stop at just water. You can use the same technique to make a river of lava for example. ### Vertex Positions Like [screen space refraction](screen-space-refraction.md), you'll need both the foreground and background vertex positions. The foreground being the scene with the foamy surface and the background being the scene without the foamy surface. Referrer back to [SSAO](ssao.md#vertex-positions) for the details on how to acquire the vertex positions in view space. ### Mask

Foam Mask

You'll need to texture your scene with a foam mask. The demo masks everything off except the water. For the water, it textures it with a foam pattern. ```c // ... uniform sampler2D foamPatternTexture; in vec2 diffuseCoord; out vec4 fragColor; void main() { vec4 foamPattern = texture(foamPatternTexture, diffuseCoord); fragColor = vec4(vec3(dot(foamPattern.rgb, vec3(1)) / 3), 1); } ``` Here you see the fragment shader that generates the foam mask. It takes a foam pattern texture and UV maps it to the scene's geometry using the diffuse UV coordinates. For every model, except the water, the shader is given a solid black texture as the `foamPatternTexture`. ```c // ... fragColor = vec4(vec3(dot(foamPattern.rgb, vec3(1)) / 3), 1); // ... ``` The fragment color is converted to greyscale, as a precaution, since the foam shader expects the foam mask to be greyscale. ### Uniforms ```c // ... uniform sampler2D maskTexture; uniform sampler2D positionFromTexture; uniform sampler2D positionToTexture; // ... ``` The foam shader accepts a mask texture, the foreground vertex positions (`positionFromTexture`), and the background vertex positions (`positionToTexture`). ### Parameters ```c // ... float foamDepth = 4; vec4 foamColor = vec4(0.8, 0.85, 0.92, 1); // ... ``` The adjustable parameters for the foam shader are the foam depth and color. The foam depth controls how much foam is shown. As the foam depth increases, the amount of foam shown increases. ### Distance ```c // ... vec4 positionFrom = texture(positionFromTexture, texCoord); vec4 positionTo = texture(positionToTexture, texCoord); float depth = (positionTo.xyz - positionFrom.xyz).y; // ... ``` Compute the distance from the foreground position to the background position. Since the positions are in view (camera) space, we only need the y value since it goes into the screen. ### Amount ```c // ... float amount = clamp(depth / foamDepth.x, 0, 1); amount = 1 - amount; amount *= mask.r; amount = amount * amount / (2 * (amount * amount - amount) + 1); // ... ``` The amount of foam is based on the depth, the foam depth parameter, and the mask value.

Easing equation.

```c // ... amount = amount * amount / (2 * (amount * amount - amount) + 1); // ... ``` Reshape the amount using the ease in and out easing function. This will give a lot of foam near depth zero and little to no foam near `foamDepth`. ### Fragment Color ```c // ... fragColor = mix(vec4(0), foamColor, amount); // ... ``` The fragment color is a mix between transparent black and the foam color based on the amount. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [foam-mask.frag](../demonstration/shaders/fragment/foam-mask.frag) - [foam.frag](../demonstration/shaders/fragment/foam.frag) - [base-combine.frag](../demonstration/shaders/fragment/base-combine.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](screen-space-refraction.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](flow-mapping.md) ================================================ FILE: sections/fog.md ================================================ [:arrow_backward:](deferred-rendering.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](blur.md) # 3D Game Shaders For Beginners ## Fog

Fog

Fog (or mist as it's called in Blender) adds atmospheric haze to a scene, providing mystique and softening pop-ins (geometry suddenly entering into the camera's frustum). ```c // ... uniform vec4 color; uniform vec2 nearFar; // ... ``` To calculate the fog, you'll need its color, near distance, and far distance. ```c // ... uniform sampler2D positionTexture; // ... vec2 texSize = textureSize(positionTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 position = texture(positionTexture, texCoord); // ... ``` In addition to the fog's attributes, you'll also need the fragment's vertex `position`. ```c float fogMin = 0.00; float fogMax = 0.97; ``` `fogMax` controls how much of the scene is still visible when the fog is most intense. `fogMin` controls how much of the fog is still visible when the fog is least intense. ```c // ... float near = nearFar.x; float far = nearFar.y; float intensity = clamp ( (position.y - near) / (far - near) , fogMin , fogMax ); // ... ``` The example code uses a linear model for calculating the fog's intensity. There's also an exponential model you could use. The fog's intensity is `fogMin` before or at the start of the fog's `near` distance. As the vertex `position` gets closer to the end of the fog's `far` distance, the `intensity` moves closer to `fogMax`. For any vertexes after the end of the fog, the `intensity` is clamped to `fogMax`. ```c // ... fragColor = vec4(color.rgb, intensity); // ... ``` Set the fragment's color to the fog `color` and the fragment's alpha channel to the `intensity`. As `intensity` gets closer to one, you'll have less of the scene's color and more of the fog color. When `intensity` reaches one, you'll have all fog color and nothing else. ```c // ... uniform sampler2D baseTexture; uniform sampler2D fogTexture; // ... vec2 texSize = textureSize(baseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 baseColor = texture(baseTexture, texCoord); vec4 fogColor = texture(fogTexture, texCoord); fragColor = baseColor; // ... fragColor = mix(fragColor, fogColor, min(fogColor.a, 1)); // ... ``` Normally you calculate the fog in the same shader that does the lighting calculations. However, you can also break it out into its own framebuffer texture. Here you see the fog color being applied to the rest of the scene much like you would apply a layer in GIMP. This allows you to calculate the fog once instead calculating it in every shader that eventually needs it. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [normal.frag](../demonstration/shaders/fragment/normal.frag) - [fog.frag](../demonstration/shaders/fragment/fog.frag) - [outline.frag](../demonstration/shaders/fragment/outline.frag) - [scene-combine.frag](../demonstration/shaders/fragment/scene-combine.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](deferred-rendering.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](blur.md) ================================================ FILE: sections/fresnel-factor.md ================================================ [:arrow_backward:](blinn-phong.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](rim-lighting.md) # 3D Game Shaders For Beginners ## Fresnel Factor

Fresnel

The fresnel factor alters the reflectiveness of a surface based on the camera or viewing angle. As a surface points away from the camera, its reflectiveness goes up. Similarly, as a surface points towards the camera, its reflectiveness goes down.

In other words, as a surface becomes perpendicular with the camera, it becomes more mirror like. Utilizing this property, you can vary the opacity of reflections (such as [specular](lighting.md#specular) and [screen space reflections](screen-space-reflection.md)) and/or vary a surface's alpha values for a more plausible or realistic look. ### Specular Reflection

Specular Intensity

```c vec4 specular = materialSpecularColor * lightSpecularColor * pow(max(dot(eye, reflection), 0.0), shininess); ``` In the [lighting](lighting.md#specular) section, the specular component was a combination of the material's specular color, the light's specular color, and by how much the camera pointed into the light's reflection direction. Incorporating the fresnel factor, you'll now vary the material specular color based on the angle between the camera and the surface it's pointed at. ```c // ... vec3 eye = normalize(-vertexPosition.xyz); // ... ``` The first vector you'll need is the eye/view/camera vector. Recall that the eye vector points from the vertex position to the camera's position. If the vertex position is in view or camera space, the eye vector is the vertex position pointed in the opposite direction. ```c // ... vec3 light = normal(lightPosition.xyz - vertexPosition.xyz); vec3 halfway = normalize(light + eye); // ... ``` The fresnel factor is calculated using two vectors. The simplest two vectors to use are the eye and normal vector. However, if you're using the halfway vector (from the [Blinn-Phong](blinn-phong.md) section), you can instead calculate the fresnel factor using the halfway and eye vector. ```c // ... float fresnelFactor = dot(halfway, eye); // Or dot(normal, eye). fresnelFactor = max(fresnelFactor, 0.0); fresnelFactor = 1.0 - fresnelFactor; fresnelFactor = pow(fresnelFactor, fresnelPower); // ... ``` With the needed vectors in hand, you can now compute the fresnel factor. The fresnel factor ranges from zero to one. When the dot product is one, the fresnel factor is zero. When the dot product is less than or equal to zero, the fresnel factor is one. This equation comes from [Schlick's approximation](https://en.wikipedia.org/wiki/Schlick%27s_approximation).

Fresnel Power

In Schlick's approximation, the `fresnelPower` is five but you can alter this to your liking. The demo code varies it using the blue channel of the specular map with a maximum value of five. ```c // ... materialSpecularColor.rgb = mix(materialSpecularColor.rgb, vec3(1.0), fresnelFactor); // ... ``` Once the fresnel factor is determined, use it to modulate the material's specular color. As the fresnel factor approaches one, the material becomes more like a mirror or fully reflective. ```c // ... vec4 specular = vec4(vec3(0.0), 1.0); specular.rgb = materialSpecularColor.rgb * lightSpecularColor.rgb * pow ( max(dot(normal, halfway), 0.0) // Or max(dot(reflection, eye), 0.0). , shininess ); // ... ``` As before, the specular component is a combination of the material's specular color, the light's specular color, and by how much the camera points into the direction of the light's reflection. However, using the fresnel factor, the material's specular color various depending on the orientation of the camera and the surface it's looking at. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](blinn-phong.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](rim-lighting.md) ================================================ FILE: sections/gamma-correction.md ================================================ [:arrow_backward:](lookup-table.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](setup.md) # 3D Game Shaders For Beginners ## Gamma Correction

Gamma Correction

Correcting for gamma will make your color calculations look correct. This isn't to say they'll look amazing but with gamma correction, you'll find that the colors meld together better, the shadows are more nuanced, and the highlights are more subtle. Without gamma correction, the shadowed areas tend to get crushed while the highlighted areas tend to get blown-out and over saturated making for a harsh contrast overall. If you're aiming for realism, gamma correction is especially important. As you perform more and more calculations, the tiny errors add up making it harder to achieve photorealism. The equations will be correct but the inputs and outputs will be wrong leaving you frustrated. It's easy to get twisted around when thinking about gamma correction but essentially it boils down to knowing what color space a color is in and how to convert that color to the color space you need. With those two pieces of the puzzle, gamma correction becomes a tedious yet simple chore you'll have to perform from time to time. ### Color Spaces

sRGB vs RGB

The two color spaces you'll need to be aware of are sRGB (standard Red Green Blue) and RGB or linear color space. ```bash identify -format "%[colorspace]\n" house-diffuse-srgb.png sRGB identify -format "%[colorspace]\n" house-diffuse-rgb.png RGB ``` Knowing what color space a color texture is in will determine how you handle it in your shaders. To determine the color space of a texture, use ImageMagick's `identify`. You'll find that most textures are in sRGB. ```bash convert house-diffuse-srgb -colorspace rgb house-diffuse-rgb.png ``` To convert a texture to a particular color space, use ImageMagick's `convert` program. Notice how a texture is darkened when transforming from sRGB to RGB. ### Decoding The red, green, and blue values in a sRGB color texture are encoded and cannot be modified directly. Modifying them directly would be like running spellcheck on an encrypted message. Before you can run spellcheck, you first have to decrypt the message. Similarly, to modify the values of an sRGB texture, you first have to decode or transform them to RGB or linear color space. ```c // ... color = texture(color_texture, uv); color.rgb = pow(color.rgb, 2.2); // ... ``` To decode a sRGB encoded color, raise the `rgb` values to the power of `2.2`. Once you have decoded the color, you are now free to add, subtract, multiply, and divide it. By raising the color values to the power of `2.2`, you're converting them from sRGB to RGB or linear color space. This conversion has the effect of darkening the colors.

Color Darkening

For example, `vec3(0.9, 0.2, 0.3)` becomes `vec3(0.793, 0.028, 0.07)`.

Gamma Curves

The `2.2` value is known as gamma. Loosely speaking, gamma can either be `1.0 / 2.2`, `2.2`, or `1.0`. As you've seen, `2.2` is for decoding sRGB encoded color textures. As you will see, `1.0 / 2.2` is for encoding linear or RGB color textures. And `1.0` is RGB or linear color space since `y = 1 * x + 0` and any base raised to the power of `1.0` is itself. #### Non-color Data

Non-color Data

One important exception to decoding is when the "colors" of a texture represent non-color data. Some examples of non-color data would be the normals in a normal map, the alpha channel, the heights in a height map, and the directions in a flow map. Only decode color related data or data that represents color. When dealing with non-color data, treat the sRGB color values as RGB or linear and skip the decoding process. ### Encoding

Perceptually versus Actually Linear

The necessity for encoding and decoding stems from the fact that humans do not perceive lightness linearly and most displays (like a monitor) lack the precision or number of bits to accurately show both lighter and darker tonal values or shades. With only so many bits to go around, colors are encoded in such a way that more bits are devoted to the darker shades than the lighter shades since humans are more sensitive to darker tones than lighter tones. Encoding it this way uses the limited number of bits more effectively for human perception. Still, the only thing to remember is that your display is expecting sRGB encoded values. Therefore, if you decoded a sRGB value, you have to encode it before it makes its way to your display. ```c // ... color = texture(color_texture, uv); color.rgb = pow(color.rgb, 1.0 / 2.2); // ... ``` To encode a linear value or convert RGB to sRGB, raise the `rgb` values to the power of `1.0 / 2.2`. Notice how `1.1 / 2.2` is the reciprocal of `2.2` or `2.2 / 1.0`. Here you see the symmetry in decoding and encoding.

Not Gamma Corrected versus Gamma Corrected

### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [gamma-correction.frag](../demonstration/shaders/fragment/gamma-correction.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](lookup-table.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](setup.md) ================================================ FILE: sections/glsl.md ================================================ [:arrow_backward:](reference-frames.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](render-to-texture.md) # 3D Game Shaders For Beginners ## GLSL

Instead of using the [fixed-function](https://en.wikipedia.org/wiki/Fixed-function) pipeline, you'll be using the programmable GPU rendering pipeline. Since it is programmable, it is up to you to supply the programming in the form of shaders. A shader is a (typically small) program you write using a syntax reminiscent of C. The programmable GPU rendering pipeline has various different stages that you can program with shaders. The different types of shaders include vertex, tessellation, geometry, fragment, and compute. You'll only need to focus on the vertex and fragment stages for the techniques below. ```c #version 150 void main() {} ``` Here is a bare-bones GLSL shader consisting of the GLSL version number and the main function. ```c #version 150 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } ``` Here is a stripped down GLSL vertex shader that transforms an incoming vertex to clip space and outputs this new position as the vertex's homogeneous position. The `main` procedure doesn't return anything since it is `void` and the `gl_Position` variable is a built-in output. Take note of the keywords `uniform` and `in`. The `uniform` keyword means this global variable is the same for all vertexes. Panda3D sets the `p3d_ModelViewProjectionMatrix` for you and it is the same matrix for each vertex. The `in` keyword means this global variable is being given to the shader. The vertex shader receives each vertex that makes up the geometry the vertex shader is attached to. ```c #version 150 out vec4 fragColor; void main() { fragColor = vec4(0, 1, 0, 1); } ``` Here is a stripped down GLSL fragment shader that outputs the fragment color as solid green. Keep in mind that a fragment affects at most one screen pixel but a single pixel can be affected by many fragments. Take note of the `out` keyword. The `out` keyword means this global variable is being set by the shader. The name `fragColor` is arbitrary so feel free to choose a different one.

Output of the stripped down shaders.

This is the output of the two shaders shown above. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](reference-frames.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](render-to-texture.md) ================================================ FILE: sections/lighting.md ================================================ [:arrow_backward:](texturing.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](blinn-phong.md) # 3D Game Shaders For Beginners ## Lighting

Lighting

Completing the lighting involves calculating and combining the ambient, diffuse, specular, and emission light aspects. The example code uses either Phong or Blinn-Phong lighting. ### Vertex ```c // ... uniform struct p3d_LightSourceParameters { vec4 color ; vec4 ambient ; vec4 diffuse ; vec4 specular ; vec4 position ; vec3 spotDirection ; float spotExponent ; float spotCutoff ; float spotCosCutoff ; float constantAttenuation ; float linearAttenuation ; float quadraticAttenuation ; vec3 attenuation ; sampler2DShadow shadowMap ; mat4 shadowViewMatrix ; } p3d_LightSource[NUMBER_OF_LIGHTS]; // ... ``` For every light, minus the ambient light, Panda3D gives you this convenient struct which is available to both the vertex and fragment shaders. The biggest convenience being the shadow map and shadow view matrix for transforming vertexes to shadow or light space. ```c // ... vertexPosition = p3d_ModelViewMatrix * p3d_Vertex; // ... for (int i = 0; i < p3d_LightSource.length(); ++i) { vertexInShadowSpaces[i] = p3d_LightSource[i].shadowViewMatrix * vertexPosition; } // ... ``` Starting in the vertex shader, you'll need to transform and output the vertex from view space to shadow or light space for each light in your scene. You'll need this later in the fragment shader in order to render the shadows. Shadow or light space is where every coordinate is relative to the light position (the light is the origin). ### Fragment The fragment shader is where most of the lighting calculations take place. #### Material ```c // ... uniform struct { vec4 ambient ; vec4 diffuse ; vec4 emission ; vec3 specular ; float shininess ; } p3d_Material; // ... ``` Panda3D gives us the material (in the form of a struct) for the mesh or model you are currently rendering. #### Multiple Lights ```c // ... vec4 diffuse = vec4(0.0, 0.0, 0.0, diffuseTex.a); vec4 specular = vec4(0.0, 0.0, 0.0, diffuseTex.a); // ... ``` Before you loop through the scene's lights, create an accumulator for both the diffuse and specular colors. ```c // ... for (int i = 0; i < p3d_LightSource.length(); ++i) { // ... } // ... ``` Now you can loop through the lights, calculating the diffuse and specular colors for each one. #### Light Related Vectors

Phong Lighting Model

Here you see the four major vectors you'll need to calculate the diffuse and specular colors contributed by each light. The light direction vector is the light blue arrow pointing to the light. The normal vector is the green arrow standing straight up. The reflection vector is the dark blue arrow mirroring the light direction vector. The eye or view vector is the orange arrow pointing towards the camera. ```c // ... vec3 lightDirection = p3d_LightSource[i].position.xyz - vertexPosition.xyz * p3d_LightSource[i].position.w; // ... ``` The light direction is from the vertex's position to the light's position. Panda3D sets `p3d_LightSource[i].position.w` to zero if this is a directional light. Directional lights do not have a position as they only have a direction. So if this is a directional light, the light direction will be the negative or opposite direction of the light as Panda3D sets `p3d_LightSource[i].position.xyz` to be `-direction` for directional lights. ```c // ... normal = normalize(vertexNormal); // ... ``` You'll need the vertex normal to be a unit vector. Unit vectors have a length of magnitude of one. ```c // ... vec3 unitLightDirection = normalize(lightDirection); vec3 eyeDirection = normalize(-vertexPosition.xyz); vec3 reflectedDirection = normalize(-reflect(unitLightDirection, normal)); // ... ``` Next you'll need three more vectors. You'll need to take the dot product involving the light direction so its best to normalize it. This gives it a distance or magnitude of one (unit vector). The eye direction is the opposite of the vertex/fragment position since the vertex/fragment position is relative to the camera's position. Remember that the vertex/fragment position is in view space. So instead of going from the camera (eye) to the vertex/fragment, you go from the vertex/fragment to the eye (camera). The [reflection vector](http://asawicki.info/news_1301_reflect_and_refract_functions.html) is a reflection of the light direction at the surface normal. As the light "ray" hits the surface, it bounces off at the same angle it came in at. The angle between the light direction vector and the normal is known as the "angle of incidence". The angle between the reflection vector and the normal is known as the "angle of reflection". You'll have to negate the reflected light vector as it needs to point in the same direction as the eye vector. Remember the eye direction is from the vertex/fragment to the camera position. You'll use the reflection vector to calculate the intensity of the specular highlight. #### Diffuse ```c // ... float diffuseIntensity = dot(normal, unitLightDirection); if (diffuseIntensity < 0.0) { continue; } // ... ``` The diffuse intensity is the dot product between the surface normal and the unit vector light direction. The dot product can range from negative one to one. If both vectors point in the same direction, the intensity is one. Any other case will be less than one.

The light direction versus the normal direction.

As the light vector approaches the same direction as the normal, the diffuse intensity approaches one. ```c // ... if (diffuseIntensity < 0.0) { continue; } // ... ``` If the diffuse intensity is zero or less, move on to the next light. ```c // ... vec4 diffuseTemp = vec4 ( clamp ( diffuseTex.rgb * p3d_LightSource[i].diffuse.rgb * diffuseIntensity , 0 , 1 ) , diffuseTex.a ); diffuseTemp = clamp(diffuseTemp, vec4(0), diffuseTex); // ... ``` You can now calculate the diffuse color contributed by this light. If the diffuse intensity is one, the diffuse color will be a mix between the diffuse texture color and the lights color. Any other intensity will cause the diffuse color to be darker. Notice how I clamp the diffuse color to be only as bright as the diffuse texture color is. This will protect the scene from being over exposed. When creating your diffuse textures, make sure to create them as if they were fully lit. #### Specular After diffuse, comes specular.

Specular Intensity

```c // ... float specularIntensity = max(dot(reflectedDirection, eyeDirection), 0); vec4 specularTemp = clamp ( vec4(p3d_Material.specular, 1) * p3d_LightSource[i].specular * pow ( specularIntensity , p3d_Material.shininess ) , 0 , 1 ); // ... ``` The specular intensity is the dot product between the eye vector and the reflection vector. As with the diffuse intensity, if the two vectors point in the same direction, the specular intensity is one. Any other intensity will diminish the amount of specular color contributed by this light.

Shininess

The material shininess determines how spread out the specular highlight is. This is typically set in a modeling program like Blender. In Blender it's known as the specular hardness. #### Spotlights ```c // ... float unitLightDirectionDelta = dot ( normalize(p3d_LightSource[i].spotDirection) , -unitLightDirection ); if (unitLightDirectionDelta < p3d_LightSource[i].spotCosCutoff) { continue; } // ... } ``` This snippet keeps fragments outside of a spotlight's cone or frustum from being affected by the light. Fortunately, Panda3D [sets up](https://github.com/panda3d/panda3d/blob/daa57733cb9b4ccdb23e28153585e8e20b5ccdb5/panda/src/display/graphicsStateGuardian.cxx#L1705) `spotDirection` and `spotCosCutoff` to also work for directional lights and points lights. Spotlights have both a position and direction. However, directional lights only have a direction and point lights only have a position. Still, this code works for all three lights avoiding the need for noisy if statements. ```c // ... , -unitLightDirection // ... ``` You must negate `unitLightDirection`. `unitLightDirection` goes from the fragment to the spotlight and you need it to go from the spotlight to the fragment since the `spotDirection` goes directly down the center of the spotlight's frustum some distance away from the spotlight's position. ```c spotCosCutoff = cosine(0.5 * spotlightLensFovAngle); ``` For a spotlight, if the dot product between the fragment-to-light vector and the spotlight's direction vector is less than the cosine of half the spotlight's field of view angle, the shader disregards this light's influence. For directional lights and point lights, Panda3D sets `spotCosCutoff` to negative one. Recall that the dot product ranges from negative one to one. So it doesn't matter what the `unitLightDirectionDelta` is because it will always be greater than or equal to negative one. ```c // ... diffuseTemp *= pow(unitLightDirectionDelta, p3d_LightSource[i].spotExponent); // ... ``` Like the `unitLightDirectionDelta` snippet, this snippet also works for all three light types. For spotlights, this will make the fragments brighter as you move closer to the center of the spotlight's frustum. For directional lights and point lights, `spotExponent` is zero. Recall that anything to the power of zero is one so the diffuse color is one times itself meaning it is unchanged. #### Shadows ```c // ... float shadow = textureProj ( p3d_LightSource[i].shadowMap , vertexInShadowSpaces[i] ); diffuseTemp.rgb *= shadow; specularTemp.rgb *= shadow; // ... ``` Panda3D makes applying shadows relatively easy by providing the shadow map and shadow transformation matrix for every scene light. To create the shadow transformation matrix yourself, you'll need to assemble a matrix that transforms view space coordinates to light space (coordinates are relative to the light's position). To create the shadow map yourself, you'll need to render the scene from the perspective of the light to a framebuffer texture. The framebuffer texture must hold the distances from the light to the fragments. This is known as a "depth map". Lastly, you'll need to manually give to your shader your DIY depth map as a `uniform sampler2DShadow` and your DIY shadow transformation matrix as a `uniform mat4`. At this point, you've recreated what Panda3D does for you automatically. The shadow snippet shown uses `textureProj` which is different from the `texure` function shown earlier. `textureProj` first divides `vertexInShadowSpaces[i].xyz` by `vertexInShadowSpaces[i].w`. After this, it uses `vertexInShadowSpaces[i].xy` to locate the depth stored in the shadow map. Next it uses `vertexInShadowSpaces[i].z` to compare this vertex's depth against the shadow map depth at `vertexInShadowSpaces[i].xy`. If the comparison passes, `textureProj` will return one. Otherwise, it will return zero. Zero meaning this vertex/fragment is in the shadow and one meaning this vertex/fragment is not in the shadow. `textureProj` can also return a value between zero and one depending on how the shadow map was set up. In this instance, `textureProj` performs multiple depth tests using neighboring depth values and returns a weighted average. This weighted average can give shadows a softer look. #### Attenuation

Attenuation

```c // ... float lightDistance = length(lightDirection); float attenuation = 1 / ( p3d_LightSource[i].constantAttenuation + p3d_LightSource[i].linearAttenuation * lightDistance + p3d_LightSource[i].quadraticAttenuation * (lightDistance * lightDistance) ); diffuseTemp.rgb *= attenuation; specularTemp.rgb *= attenuation; // ... ``` The light's distance is just the magnitude or length of the light direction vector. Notice it's not using the normalized light direction as that distance would be one. You'll need the light distance to calculate the attenuation. Attenuation meaning the light's influence diminishes as you get further away from it. You can set `constantAttenuation`, `linearAttenuation`, and `quadraticAttenuation` to whatever values you would like. A good starting point is `constantAttenuation = 1`, `linearAttenuation = 0`, and `quadraticAttenuation = 1`. With these settings, the attenuation is one at the light's position and approaches zero as you move further away. #### Final Light Color ```c // ... diffuse += diffuseTemp; specular += specularTemp; // ... ``` To calculate the final light color, add the diffuse and specular together. Be sure to add this to the accumulator as you loop through the scene's lights. #### Ambient ```c // ... uniform sampler2D p3d_Texture1; // ... uniform struct { vec4 ambient ; } p3d_LightModel; // ... in vec2 diffuseCoord; // ... vec4 diffuseTex = texture(p3d_Texture1, diffuseCoord); // ... vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex; // ... ``` The ambient component to the lighting model is based on the material's ambient color, the ambient light's color, and the diffuse texture color. There should only ever be one ambient light. Because of this, the ambient color calculation only needs to occur once. Contrast this with the diffuse and specular color which must be accumulated for each spot/directional/point light. When you reach [SSAO](ssao.md), you'll revisit the ambient color calculation. #### Putting It All Together ```c // ... vec4 outputColor = ambient + diffuse + specular + p3d_Material.emission; // ... ``` The final color is the sum of the ambient color, diffuse color, specular color, and the emission color. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](texturing.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](blinn-phong.md) ================================================ FILE: sections/lookup-table.md ================================================ [:arrow_backward:](film-grain.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](gamma-correction.md) # 3D Game Shaders For Beginners ## Lookup Table (LUT)

LUT

The lookup table or LUT shader allows you to transform the colors of your game using an image editor like the [GIMP](https://www.gimp.org/). From color grading to turning day into night, the LUT shader is a handy tool for tweaking the look of your game.

Neutral LUT

Before you can get started, you'll need to find a neutral LUT image. Neutral meaning that it leaves the fragment colors unchanged. The LUT needs to be 256 pixels wide by 16 pixels tall and contain 16 blocks with each block being 16 by 16 pixels. The LUT is mapped out into 16 blocks. Each block has a different level of blue. As you move across the blocks, from left to right, the amount of blue increases. You can see the amount of blue in each block's upper-left corner. Within each block, the amount of red increases as you move from left to right and the amount of green increases as you move from top to bottom. The upper-left corner of the first block is black since every RGB channel is zero. The lower-right corner of the last block is white since every RGB channel is one.

LUT And Screenshot

With the neutral LUT in hand, take a screenshot of your game and open it in your image editor. Add the neutral LUT as a new layer and merge it with the screenshot. As you manipulate the colors of the screenshot, the LUT will be altered in the same way. When you're done editing, select only the LUT and save it as a new image. You now have your new lookup table and can begin writing your shader. ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec4 color = texture(colorTexture, gl_FragCoord.xy / texSize); // ... ``` The LUT shader is a screen space technique. Therefore, sample the scene's color at the current fragment or screen position. ```c // ... float u = floor(color.b * 15.0) / 15.0 * 240.0; u = (floor(color.r * 15.0) / 15.0 * 15.0) + u; u /= 255.0; float v = ceil(color.g * 15.0); v /= 15.0; v = 1.0 - v; // ... ``` In order to transform the current fragment's color, using the LUT, you'll need to map the color to two UV coordinates on the lookup table texture. The first mapping (shown up above) is to the nearest left or lower bound block location and the second mapping (shown below) is to the nearest right or upper bound block mapping. At the end, you'll combine these two mappings to create the final color transformation.

RGB Channel Mapping

Each of the red, green, and blue channels maps to one of 16 possibilities in the LUT. The blue channel maps to one of the 16 upper-left block corners. After the blue channel maps to a block, the red channel maps to one of the 16 horizontal pixel positions within the block and the green channel maps to one of the 16 vertical pixel positions within the block. These three mappings will determine the UV coordinate you'll need to sample a color from the LUT. ```c // ... u /= 255.0; v /= 15.0; v = 1.0 - v; // ... ``` To calculate the final U coordinate, divide it by 255 since the LUT is 256 pixels wide and U ranges from zero to one. To calculate the final V coordinate, divide it by 15 since the LUT is 16 pixels tall and V ranges from zero to one. You'll also need to subtract the normalized V coordinate from one since V ranges from zero at the bottom to one at the top while the green channel ranges from zero at the top to 15 at the bottom. ```c // ... vec3 left = texture(lookupTableTexture, vec2(u, v)).rgb; // ... ``` Using the UV coordinates, sample a color from the lookup table. This is the nearest left block color. ```c // ... u = ceil(color.b * 15.0) / 15.0 * 240.0; u = (ceil(color.r * 15.0) / 15.0 * 15.0) + u; u /= 255.0; v = 1.0 - (ceil(color.g * 15.0) / 15.0); vec3 right = texture(lookupTableTexture, vec2(u, v)).rgb; // ... ``` Now you'll need to calculate the UV coordinates for the nearest right block color. Notice how `ceil` or ceiling is being used now instead of `floor`.

Mixing

```c // ... color.r = mix(left.r, right.r, fract(color.r * 15.0)); color.g = mix(left.g, right.g, fract(color.g * 15.0)); color.b = mix(left.b, right.b, fract(color.b * 15.0)); // ... ``` Not every channel will map perfectly to one of its 16 possibilities. For example, `0.5` doesn't map perfectly. At the lower bound (`floor`), it maps to `0.4666666666666667` and at the upper bound (`ceil`), it maps to `0.5333333333333333`. Compare that with `0.4` which maps to `0.4` at the lower bound and `0.4` at the upper bound. For those channels which do not map perfectly, you'll need to mix the left and right sides based on where the channel falls between its lower and upper bound. For `0.5`, it falls directly between them making the final color a mixture of half left and half right. However, for `0.132` the mixture will be 98% right and 2% left since the fractional part of `0.123` times `15.0` is `0.98`. ```c // ... fragColor = color; // ... ``` Set the fragment color to the final mix and you're done. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [lookup-table.frag](../demonstration/shaders/fragment/lookup-table.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](film-grain.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](gamma-correction.md) ================================================ FILE: sections/motion-blur.md ================================================ [:arrow_backward:](ssao.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](chromatic-aberration.md) # 3D Game Shaders For Beginners ## Motion Blur

Motion Blur

To really sell the illusion of speed, you can do no better than motion blur. From high speed car chases to moving at warp speed, motion blur greatly improves the look and feel of fast moving objects. There are a few ways to implement motion blur as a screen space technique. The less involved implementation will only blur the scene in relation to the camera's movements while the more involved version will blur any moving objects even with the camera remaining still. The less involved technique is described below but the principle is the same. ### Textures ```c uniform sampler2D positionTexture; uniform sampler2D colorTexture; // ... ``` The input textures needed are the vertex positions in view space and the scene's colors. Refer back to [SSAO](ssao.md#vertex-positions) for acquiring the vertex positions. ### Matrices ```c // ... uniform mat4 previousViewWorldMat; uniform mat4 worldViewMat; uniform mat4 lensProjection; // ... ``` The motion blur technique determines the blur direction by comparing the previous frame's vertex positions with the current frame's vertex positions. To do this, you'll need the previous frame's view-to-world matrix, the current frame's world-to-view matrix, and the camera lens' projection matrix. ### Parameters ```c // ... uniform vec2 parameters; // ... void main() { int size = int(parameters.x); float separation = parameters.y; // ... ``` The adjustable parameters are `size` and `separation`. `size` controls how many samples are taken along the blur direction. Increasing `size` increases the amount of blur at the cost of performance. `separation` controls how spread out the samples are along the blur direction. Increasing `separation` increases the amount of blur at the cost of accuracy. ### Blur Direction ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 position1 = texture(positionTexture, texCoord); vec4 position0 = worldViewMat * previousViewWorldMat * position1; // ... ``` To determine which way to blur this fragment, you'll need to know where things were last frame and where things are this frame. To figure out where things are now, sample the current vertex position. To figure out where things were last frame, transform the current position from view space to world space, using the previous frame's view-to-world matrix, and then transform it back to view space from world space using this frame's world-to-view matrix. This transformed position is this fragment's previous interpolated vertex position.

Position Projection

```c // ... position0 = lensProjection * position0; position0.xyz /= position0.w; position0.xy = position0.xy * 0.5 + 0.5; position1 = lensProjection * position1; position1.xyz /= position1.w; position1.xy = position1.xy * 0.5 + 0.5; // ... ``` Now that you have the current and previous positions, transform them to screen space. With the positions in screen space, you can trace out the 2D direction you'll need to blur the onscreen image. ```c // ... // position1.xy = position0.xy + direction; vec2 direction = position1.xy - position0.xy; if (length(direction) <= 0.0) { return; } // ... ``` The blur direction goes from the previous position to the current position. ### Blurring ```c // ... fragColor = texture(colorTexture, texCoord); // ... ``` Sample the current fragment's color. This will be the first of the colors blurred together. ```c // ... direction.xy *= separation; // ... ``` Multiply the direction vector by the separation. ```c // ... vec2 forward = texCoord; vec2 backward = texCoord; // ... ``` For a more seamless blur, sample in the direction of the blur and in the opposite direction of the blur. For now, set the two vectors to the fragment's UV coordinate. ```c // ... float count = 1.0; // ... ``` `count` is used to average all of the samples taken. It starts at one since you've already sampled the current fragment's color. ```c // ... for (int i = 0; i < size; ++i) { forward += direction; backward -= direction; fragColor += texture ( colorTexture , forward ); fragColor += texture ( colorTexture , backward ); count += 2.0; } // ... ``` Sample the screen's colors both in the forward and backward direction of the blur. Be sure to add these samples together as you travel along. ```c // ... fragColor /= count; } ``` The final fragment color is the average color of the samples taken. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [motion-blur.frag](../demonstration/shaders/fragment/motion-blur.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](ssao.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](chromatic-aberration.md) ================================================ FILE: sections/normal-mapping.md ================================================ [:arrow_backward:](cel-shading.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](deferred-rendering.md) # 3D Game Shaders For Beginners ## Normal Mapping

Normal Mapping

Normal mapping allows you to add surface details without adding any geometry. Typically, in a modeling program like Blender, you create a high poly and a low poly version of your mesh. You take the vertex normals from the high poly mesh and bake them into a texture. This texture is the normal map. Then inside the fragment shader, you replace the low poly mesh's vertex normals with the high poly mesh's normals you baked into the normal map. Now when you light your mesh, it will appear to have more polygons than it really has. This will keep your FPS high while at the same time retain most of the details from the high poly version.

From high to low poly with normal mapping.

Here you see the progression from the high poly model to the low poly model to the low poly model with the normal map applied.

Normal Map Illusion

Keep in mind though, normal mapping is only an illusion. After a certain angle, the surface will look flat again. ### Vertex ```c // ... uniform mat3 p3d_NormalMatrix; // ... in vec3 p3d_Normal; // ... in vec3 p3d_Binormal; in vec3 p3d_Tangent; // ... vertexNormal = normalize(p3d_NormalMatrix * p3d_Normal); binormal = normalize(p3d_NormalMatrix * p3d_Binormal); tangent = normalize(p3d_NormalMatrix * p3d_Tangent); // ... ``` Starting in the vertex shader, you'll need to output to the fragment shader the normal vector, binormal vector, and the tangent vector. These vectors are used, in the fragment shader, to transform the normal map normal from tangent space to view space. `p3d_NormalMatrix` transforms the vertex normal, binormal, and tangent vectors to view space. Remember that in view space, all of the coordinates are relative to the camera's position.
[p3d_NormalMatrix] is the upper 3x3 of the inverse transpose of the ModelViewMatrix. It is used to transform the normal vector into view-space coordinates.

```c // ... in vec2 p3d_MultiTexCoord0; // ... out vec2 normalCoord; // ... normalCoord = p3d_MultiTexCoord0; // ... ```

Normal Maps

You'll also need to output, to the fragment shader, the UV coordinates for the normal map. ### Fragment Recall that the vertex normal was used to calculate the lighting. However, the normal map provides us with different normals to use when calculating the lighting. In the fragment shader, you need to swap out the vertex normals for the normals found in the normal map. ```c // ... uniform sampler2D p3d_Texture1; // ... in vec2 normalCoord; // ... /* Find */ vec4 normalTex = texture(p3d_Texture1, normalCoord); // ... ``` Using the normal map coordinates the vertex shader sent, pull out the normal from the normal map. ```c // ... vec3 normal; // ... /* Unpack */ normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); // ... ``` Earlier I showed how the normals are mapped to colors to create the normal map. Now this process needs to be reversed so you can get back the original normals that were baked into the map. ```c (r, g, b) = ( r * 2 - 1 , g * 2 - 1 , b * 2 - 1 ) = (x, y, z) ``` Here's the process for unpacking the normals from the normal map. ```c // ... /* Transform */ normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); // ... ``` The normals you get back from the normal map are typically in tangent space. They could be in another space, however. For example, Blender allows you to bake the normals in tangent, object, world, or camera space.

Replacing the vertex normals with the normal map normals.

To take the normal map normal from tangent space to view pace, construct a three by three matrix using the tangent, binormal, and vertex normal vectors. Multiply the normal by this matrix and be sure to normalize it. At this point, you're done. The rest of the lighting calculations are the same. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](cel-shading.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](deferred-rendering.md) ================================================ FILE: sections/outlining.md ================================================ [:arrow_backward:](flow-mapping.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](depth-of-field.md) # 3D Game Shaders For Beginners ## Outlining

Outlined Scene

Outlining your scene's geometry can give your game a distinctive look, reminiscent of comic books and cartoons. ### Discontinuities The process of outlining is the process of finding and labeling discontinuities or differences. Every time you find what you consider a significant difference, you mark it with your line color. As you go about labeling or coloring in the differences, outlines or edges will start to form. Where you choose to search for the discontinuities is up to you. It could be the diffuse colors in your scene, the normals of your models, the depth buffer, or some other scene related data. The demo uses the interpolated vertex positions to render the outlines. However, a less straightforward but more typical way is to use both the scene's normals and depth buffer values to construct the outlines. ### Vertex Positions ```c // ... uniform sampler2D positionTexture; // ... ``` Like SSAO, you'll need the vertex positions in view space. Referrer back to [SSAO](ssao.md#vertex-positions) for details. ### Scene Colors ```c // ... uniform sampler2D colorTexture; // ... ``` The demo darkens the colors of the scene where there's an outline. This tends to look nicer than a constant color since it provides some color variation to the edges. ### Parameters ```c // ... float minSeparation = 1.0; float maxSeparation = 3.0; float minDistance = 0.5; float maxDistance = 2.0; int size = 1; vec3 colorModifier = vec3(0.324, 0.063, 0.099); // ... ``` The min and max separation parameters control the thickness of the outline depending on the fragment's distance from the camera or depth. The min and max distance control the significance of any changes found. The `size` parameter controls the constant thickness of the line no matter the fragment's position. The outline color is based on `colorModifier` and the current fragment's color. ### Fragment Position ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 fragCoord = gl_FragCoord.xy; vec2 texCoord = fragCoord / texSize; vec4 position = texture(positionTexture, texCoord); // ... ``` Sample the position texture for the current fragment's position in the scene. Recall that the position texture is just a screen shaped quad making the UV coordinate the current fragment's screen coordinate divided by the dimensions of the screen. ### Fragment Depth ```c // ... float depth = clamp ( 1.0 - ( (far - position.y) / (far - near) ) , 0.0 , 1.0 ); float separation = mix(maxSeparation, minSeparation, depth); // ... ``` The fragment's depth ranges from zero to one. When the fragment's view-space y coordinate matches the far clipping plane, the depth is one. When the fragment's view-space y coordinate matches the near clipping plane, the depth is zero. In other words, the depth ranges from zero at the near clipping plane all the way up to one at the far clipping plane. ```c // ... float separation = mix(maxSeparation, minSeparation, depth); // ... ``` Converting the position to a depth value isn't necessary but it allows you to vary the thickness of the outline based on how far away the fragment is from the camera. Far away fragments get a thinner line while nearer fragments get a thicker outline. This tends to look nicer than a constant thickness since it gives depth to the outline. ### Finding The Discontinuities

Edge Finding

```c // ... float mx = 0.0; for (int i = -size; i <= size; ++i) { for (int j = -size; j <= size; ++j) { // ... } } // ... ``` Now that you have the current fragment's position, loop through an i by j grid or window surrounding the current fragment. ```c // ... texCoord = ( fragCoord + (vec2(i, j) * separation) ) / texSize; vec4 positionTemp = texture ( positionTexture , texCoord ); mx = max(mx, abs(position.y - positionTemp.y)); // ... ``` With each iteration, find the biggest distance between this fragment's and the surrounding fragments' positions.

smoothstep

```c // ... float diff = smoothstep(minDistance, maxDistance, mx); // ... ``` Calculate the significance of any difference discovered using the `minDistance`, `maxDistance`, and `smoothstep`. `smoothstep` returns values from zero to one. The `minDistance` is the left-most edge. Any difference less than the minimum distance will be zero. The `maxDistance` is the right-most edge. Any difference greater than the maximum distance will be one. For distances between the edges, the difference will be between zero and one. These values are interpolated along a s-shaped curve. ### Line Color ```c // ... vec3 lineColor = texture(colorTexture, texCoord).rgb * colorModifier; // ... ``` The line color is the current fragment color either darkened or lightened. ### Fragment Color

Outlines

```c // ... fragColor.rgb = vec4(lineColor, diff); // ... ``` The fragment's RGB color is the `lineColor` and its alpha channel is `diff`. ### Sketchy

Sketchy Outline

For a sketchy outline, you can distort the UV coordinates used to sample the position vectors.

Outline Noise

```c // ... uniform sampler2D noiseTexture; // ... ``` Start by creating a RGB noise texture. A good size is either 128 by 128 or 512 by 512. Be sure to blur it and make it tileable. This will produce a nice wavy, inky outline. ```c // ... float noiseScale = 10.0; // ... ``` The `noiseScale` parameter controls how distorted the outline is. The bigger the `noiseScale`, the sketchier the line. ```c // ... vec2 fragCoord = gl_FragCoord.xy; vec2 noise = texture(noiseTexture, fragCoord / textureSize(noiseTexture, 0).xy).rb; noise = noise * 2.0 - 1.0; noise *= noiseScale; // ... ``` Sample the noise texture using the current screen/fragment position and the size of the noise texture. Since you're distorting the UV coordinates used to sample the position vectors, you'll only need two of the three color channels. Map the two color channels from `[0, 1]` to `[-1, 1]`. Finally, scale the noise by the scale chosen earlier. ```c // ... vec2 texSize = textureSize(colorTexture, 0).xy; vec2 texCoord = (fragCoord - noise) / texSize; vec4 position = texture(positionTexture, texCoord); // ... ``` When sampling the current position, subtract the noise vector from the current fragment's coordinates.

Squiggly Outline

You could instead add it to the current fragment's coordinates which will create more of a squiggly line that loosely follows the geometry. ```c // ... texCoord = (vec2(i, j) * separation + fragCoord + noise) / texSize; // ... vec4 positionTemp = texture ( positionTexture , texCoord ); // ... ``` When sampling the surrounding positions inside the loop, add the noise vector to the current fragment's coordinates. The rest of the calculations are the same. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [outline.frag](../demonstration/shaders/fragment/outline.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](flow-mapping.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](depth-of-field.md) ================================================ FILE: sections/pixelization.md ================================================ [:arrow_backward:](posterization.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](sharpen.md) # 3D Game Shaders For Beginners ## Pixelization

Pixelization

Pixelizing your 3D game can give it a interesting look and possibly save you time by not having to create all of the pixel art by hand. Combine it with the posterization for a true retro look. ```c // ... int pixelSize = 5; // ... ``` Feel free to adjust the pixel size. The bigger the pixel size, the blockier the image will be.

Pixelization Process

```c // ... float x = int(gl_FragCoord.x) % pixelSize; float y = int(gl_FragCoord.y) % pixelSize; x = floor(pixelSize / 2.0) - x; y = floor(pixelSize / 2.0) - y; x = gl_FragCoord.x + x; y = gl_FragCoord.y + y; // ... ``` The technique works by mapping each fragment to the center of its closest, non-overlapping pixel-sized window. These windows are laid out in a grid over the input texture. The center-of-the-window fragments determine the color for the other fragments in their window. ```c // ... fragColor = texture(colorTexture, vec2(x, y) / texSize); // ... ``` Once you have determined the correct fragment coordinate to use, pull its color from the input texture and assign that to the fragment color. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [pixelize.frag](../demonstration/shaders/fragment/pixelize.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](posterization.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](sharpen.md) ================================================ FILE: sections/posterization.md ================================================ [:arrow_backward:](depth-of-field.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](pixelization.md) # 3D Game Shaders For Beginners ## Posterization

Posterization

Posterization or color quantization is the process of reducing the number of unique colors in an image. You can use this shader to give your game a comic book or retro look. Combine it with [outlining](outlining.md) for a full-on cartoon art style. There are various different ways to implement posterization. This method works directly with the greyscale values and indirectly with the RGB values of the image. For each fragment, it maps the RGB color to a greyscale value. This greyscale value is then mapped to both its lower and upper level value. The closest level to the original greyscale value is then mapped back to an RGB value This new RGB value becomes the fragment color. I find this method produces nicer results than the more typical methods you'll find. ```c // ... float levels = 10; // ... ``` The `levels` parameter controls how many discrete bands or steps there are. This will break up the continuous values from zero to one into chunks. With four levels, `0.0` to `1.0` becomes `0.0`, `0.25`, `0.5`, `0.75`, and `1.0`. ```c // ... fragColor = texture(posterizeTexture, texCoord); // ... ``` Sample the current fragment's color. ```c // ... float greyscale = max(fragColor.r, max(fragColor.g, fragColor.b)); // ... ``` Map the RGB values to a greyscale value. In this instance, the greyscale value is the maximum value of the R, G, and B values. ```c // ... float lower = floor(greyscale * levels) / levels; float lowerDiff = abs(greyscale - lower); // ... ``` Map the greyscale value to its lower level and then calculate the difference between its lower level and itself. For example, if the greyscale value is `0.87` and there are four levels, its lower level is `0.75` and the difference is `0.12`. ```c // ... float upper = ceil(greyscale * levels) / levels; float upperDiff = abs(upper - greyscale); // ... ``` Now calculate the upper level and the difference. Keeping with the example up above, the upper level is `1.0` and the difference is `0.13`. ```c // ... float level = lowerDiff <= upperDiff ? lower : upper; float adjustment = level / greyscale; // ... ``` The closest level is used to calculate the adjustment. The adjustment is the ratio between the quantized and unquantized greyscale value. This adjustment is used to map the quantized greyscale value back to an RGB value. ```c // ... fragColor.rgb * adjustment; // ... ``` After multiplying `rgb` by the adjustment, `max(r, max(g, b))` will now equal the quantized greyscale value. This maps the quantized greyscale value back to a red, green, and blue vector. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [posterize.frag](../demonstration/shaders/fragment/posterize.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](depth-of-field.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](pixelization.md) ================================================ FILE: sections/reference-frames.md ================================================ [:arrow_backward:](running-the-demo.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](glsl.md) # 3D Game Shaders For Beginners ## Reference Frames Before you write any shaders, you should be familiar with the following frames of reference or coordinate systems. All of them boil down to what origin `(0, 0, 0)` are these coordinates currently relative to? Once you know that, you can then transform them, via some matrix, to some other vector space if need be. Typically, when the output of some shader looks wrong, it's because of some coordinate system mix up. ### Model

Model Space

The model or object coordinate system is relative to the origin of the model. This is typically set to the center of the model's geometry in a modeling program like Blender. ### World

World Space

The world space is relative to the origin of the scene/level/universe that you've created. ### View

View Space

The view or eye coordinate space is relative to the position of the active camera. ### Clip

Clip Space

The clip space is relative to the center of the camera's film. All coordinates are now homogeneous, ranging from negative one to one `(-1, 1)`. X and y are parallel with the camera's film and the z coordinate is the depth.

Frustum

Any vertex not within the bounds of the camera's frustum or view volume is clipped or discarded. You can see this happening with the cube towards the back, clipped by the camera's far plane, and the cube off to the side. ### Screen

Screen Space

The screen space is (typically) relative to the lower left corner of the screen. X goes from zero to the screen width. Y goes from zero to the screen height. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](running-the-demo.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](glsl.md) ================================================ FILE: sections/render-to-texture.md ================================================ [:arrow_backward:](glsl.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](texturing.md) # 3D Game Shaders For Beginners ## Render To Texture Instead of rendering/drawing/painting directly to the screen, the example code uses a technique called "render to texture". In order to render to a texture, you'll need to set up a framebuffer and bind a texture to it. Multiple textures can be bound to a single framebuffer. The textures bound to the framebuffer hold the vector(s) returned by the fragment shader. Typically these vectors are color vectors `(r, g, b, a)` but they could also be position or normal vectors `(x, y, z, w)`. For each bound texture, the fragment shader can output a different vector. For example you could output a vertex's position and normal in a single pass. Most of the example code dealing with Panda3D involves setting up [framebuffer textures](https://www.panda3d.org/manual/?title=Render-to-Texture_and_Image_Postprocessing). To keep things straightforward, nearly all of the fragment shaders in the example code have only one output. However, you'll want to output as much as you can each render pass to keep your frames per second (FPS) high. There are two framebuffer texture setups found in the example code.

The first framebuffer texture setup.

The first setup renders the mill scene into a framebuffer texture using a variety of vertex and fragment shaders. This setup will go through each of the mill scene's vertexes and corresponding fragments. In this setup, the example code performs the following. - Stores geometry data (like vertex position or normal) for later use. - Stores material data (like the diffuse color) for later use. - UV maps the various textures (diffuse, normal, shadow, etc.). - Calculates the ambient, diffuse, specular, and emission lighting.

The second framebuffer texture setup.

The second setup is an orthographic camera pointed at a screen-shaped rectangle. This setup will go through just the four vertexes and their corresponding fragments. In this second setup, the example code performs the following. - Manipulates the output of another framebuffer texture. - Combines various framebuffer textures into one. I like to think of this second setup as using layers in GIMP, Krita, or Inkscape.

Tabbing Through Framebuffer Textures

In the example code, you can see the output of a particular framebuffer texture by using the Tab key or the Shift+Tab keys. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](glsl.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](texturing.md) ================================================ FILE: sections/rim-lighting.md ================================================ [:arrow_backward:](fresnel-factor.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](cel-shading.md) # 3D Game Shaders For Beginners ## Rim Lighting

Rim Lighting

Taking inspiration from the [fresnel factor](fresnel-factor.md), rim lighting targets the rim or silhouette of an object. When combined with [cel shading](cel-shading.md) and [outlining](outlining.md), it can really complete that cartoon look. You can also use it to highlight objects in the game, making it easier for players to navigate and accomplish tasks. ```c // ... vec3 eye = normalize(-vertexPosition.xyz); // ... ``` As it was for the fresnel factor, you'll need the eye vector. If your vertex positions are in view space, the eye vector is the negation of the vertex position.

Rim Light

```c // ... float rimLightIntensity = dot(eye, normal); rimLightIntensity = 1.0 - rimLightIntensity; rimLightIntensity = max(0.0, rimLightIntensity); // ... ``` The Intensity of the rim light ranges from zero to one. When the eye and normal vector point in the same direction, the rim light intensity is zero. As the two vectors start to point in different directions, the rim light intensity increases until it eventually reaches one when the eye and normal become orthogonal or perpendicular to one another.

Rim Light Power

```c // ... rimLightIntensity = pow(rimLightIntensity, rimLightPower); // ... ``` You can control the falloff of the rim light using the power function. ```c // ... rimLightIntensity = smoothstep(0.3, 0.4, rimLightIntensity) // ... ``` `step` or `smoothstep` can also be used to control the falloff. This tends to look better when using [cel shading](cel-shading.md). You'll learn more about these functions in later sections. ```c // ... vec4 rimLight = rimLightIntensity * diffuse; rimLight.a = diffuse.a; // ... ``` What color you use for the rim light is up to you. The demo code multiplies the diffuse light by the `rimLightIntensity`. This will highlight the silhouette without overexposing it and without lighting any shadowed fragments. ```c // ... vec4 outputColor = vec4(0.0); outputColor.a = diffuseColor.a; outputColor.rgb = ambient.rgb + diffuse.rgb + specular.rgb + rimLight.rgb + emission.rgb; // ... ``` After you've calculated the rim light, add it to the ambient, diffuse, specular, and emission lights. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) ## Copyright (C) 2020 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](fresnel-factor.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](cel-shading.md) ================================================ FILE: sections/running-the-demo.md ================================================ [:arrow_backward:](building-the-demo.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](reference-frames.md) # 3D Game Shaders For Beginners ## Running The Demo

Running The Demo

After you've built the example code, you can now run the executable or demo. ```bash ./3d-game-shaders-for-beginners ``` Here's how you run it on Linux or Mac. ```bash 3d-game-shaders-for-beginners.exe ``` Here's how you run it on Windows. ### Demo Controls The demo comes with both keyboard and mouse controls to move the camera around, toggle on and off the different effects, adjust the fog, and view the various different framebuffer textures. #### Mouse You can rotate the scene around by holding down the Left Mouse button and dragging. Hold down the Right Mouse button and drag to move up, down, left, and/or right. To zoom in, roll the Mouse Wheel forward. To zoom out, roll the Mouse Wheel backward. You can also change the focus point using the mouse. To change the focus point, click anywhere on the scene using the Middle Mouse button. #### Keyboard - w to rotate the scene down. - a to rotate the scene clockwise. - s to rotate the scene up. - d to rotate the scene counterclockwise. - z to zoom in to the scene. - x to zoom out of the scene. - to move left. - to move right. - to move up. - to move down.

- 1 to show midday. - 2 to show midnight.

- Delete to toggle the sound. - 3 to toggle fresnel. - 4 to toggle rim lighting. - 5 to toggle particles. - 6 to toggle motion blur. - 7 to toggle Kuwahara filtering. - 8 to toggle cel shading. - 9 to toggle lookup table processing. - 0 to toggle between Phong and Blinn-Phong. - y to toggle SSAO. - u to toggle outlining. - i to toggle bloom. - o to toggle normal mapping. - p to toggle fog. - h to toggle depth of field. - j to toggle posterization. - k to toggle pixelization. - l to toggle sharpen. - n to toggle film grain. - m to toggle screen space reflection. - , to toggle screen space refraction. - . to toggle flow mapping. - / to toggle the sun animation. - \\ to toggle chromatic aberration.

- r to reset the scene.

- \[ to decrease the fog near distance. - Shift+\[ to increase the fog near distance. - ] to increase the fog far distance. - Shift+] to decrease the fog far distance.

- Shift+- to decrease the amount of foam. - - to increase the amount of foam.

- Shift+= to decrease the relative index of refraction. - = to increase the relative index of refraction.

- Tab to move forward through the framebuffer textures. - Shift+Tab to move backward through the framebuffer textures. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](building-the-demo.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](reference-frames.md) ================================================ FILE: sections/screen-space-reflection.md ================================================ [:arrow_backward:](chromatic-aberration.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](screen-space-refraction.md) # 3D Game Shaders For Beginners ## Screen Space Reflection (SSR)

Screen Space Reflections

Adding reflections can really ground your scene. Wet and shiny objects spring to life as nothing makes something look wet or shiny quite like reflections. With reflections, you can really sell the illusion of water and metallic objects. In the [lighting](lighting.md) section, you simulated the reflected, mirror-like image of the light source. This was the process of rendering the specular reflection. Recall that the specular light was computed using the reflected light direction. Similarly, using screen space reflection or SSR, you can simulate the reflection of other objects in the scene instead of just the light source. Instead of the light ray coming from the source and bouncing off into the camera, the light ray comes from some object in the scene and bounces off into the camera. SSR works by reflecting the screen image onto itself using only itself. Compare this to cube mapping which uses six screens or textures. In cube mapping, you reflect a ray from some point in your scene to some point on the inside of a cube surrounding your scene. In SSR, you reflect a ray from some point on your screen to some other point on your screen. By reflecting your screen onto itself, you can create the illusion of reflection. This illusion holds for the most part but SSR does fail in some cases as you'll see. ### Ray Marching Screen space reflection uses a technique known as ray marching to determine the reflection for each fragment. Ray marching is the process of iteratively extending or contracting the length or magnitude of some vector in order to probe or sample some space for information. The ray in screen space reflection is the position vector reflected about the normal. Intuitively, a light ray hits some point in the scene, bounces off, travels in the opposite direction of the reflected position vector, bounces off the current fragment, travels in the opposite direction of the position vector, and hits the camera lens allowing you to see the color of some point in the scene reflected in the current fragment. SSR is the process of tracing the light ray's path in reverse. It tries to find the reflected point the light ray bounced off of and hit the current fragment. With each iteration, the algorithm samples the scene's positions or depths, along the reflection ray, asking each time if the ray intersected with the scene's geometry. If there is an intersection, that position in the scene is a potential candidate for being reflected by the current fragment. Ideally there would be some analytical method for determining the first intersection point exactly. This first intersection point is the only valid point to reflect in the current fragment. Instead, this method is more like a game of battleship. You can't see the intersections (if there are any) so you start at the base of the reflection ray and call out coordinates as you travel in the direction of the reflection. With each call, you get back an answer of whether or not you hit something. If you do hit something, you try points around that area hoping to find the exact point of intersection.

SSR Ray Marching

Here you see ray marching being used to calculate each fragment's reflected point. The vertex normal is the bright green arrow, the position vector is the bright blue arrow, and the bright red vector is the reflection ray marching through view space. ### Vertex Positions Like SSAO, you'll need the vertex positions in view space. Referrer back to [SSAO](ssao.md#vertex-positions) for details. ### Vertex Normals To compute the reflections, you'll need the vertex normals in view space. Referrer back to [SSAO](ssao.md#vertex-normals) for details.

SSR using normal maps.

Here you see SSR using the normal mapped normals instead of the vertex normals. Notice how the reflection follows the ripples in the water versus the more mirror like reflection shown earlier. To use the normal maps instead, you'll need to transform the normal mapped normals from tangent space to view space just like you did in the lighting calculations. You can see this being done in [normal.frag](../demonstration/shaders/fragment/normal.frag). ### Position Transformations

Position Transformations

Just like [SSAO](ssao.md), SSR goes back and forth between the screen and view space. You'll need the camera lens' projection matrix to transform points in view space to clip space. From clip space, you'll have to transform the points again to UV space. Once in UV space, you can sample a vertex/fragment position from the scene which will be the closest position in the scene to your sample. This is the _screen space_ part in _screen space reflection_ since the "screen" is a texture UV mapped over a screen shaped rectangle. ### Reflected UV Coordinates There are a few ways you can implement SSR. The example code starts the reflection process by computing a reflected UV coordinate for each screen fragment. You could skip this part and go straight to computing the reflected color instead, using the final rendering of the scene. Recall that UV coordinates range from zero to one for both U and V. The screen is just a 2D texture UV mapped over a screen-sized rectangle. Knowing this, the example code doesn't actually need the final rendering of the scene to compute the reflections. It can instead calculate what UV coordinate each screen pixel will eventually use. These calculated UV coordinates can be saved to a framebuffer texture and used later when the scene has been rendered.

Reflected UVs

Here you see the reflected UV coordinates. Without even rendering the scene yet, you can get a good feel for what the reflections will look like. ```c //... uniform mat4 lensProjection; uniform sampler2D positionTexture; uniform sampler2D normalTexture; //... ``` You'll need the camera lens' projection matrix as well as the interpolated vertex positions and normals in view space. ```c // ... float maxDistance = 15; float resolution = 0.3; int steps = 10; float thickness = 0.5; // ... ``` Like the other effects, SSR has a few parameters you can adjust. Depending on the complexity of the scene, it may take you awhile to find the right settings. Getting screen space reflections to look just right tends to be difficult when reflecting complex geometry. The `maxDistance` parameter controls how far a fragment can reflect. In other words, it controls the maximum length or magnitude of the reflection ray. The `resolution` parameter controls how many fragments are skipped while traveling or marching the reflection ray during the first pass. This first pass is to find a point along the ray's direction where the ray enters or goes behind some geometry in the scene. Think of this first pass as the rough pass. Note that the `resolution` ranges from zero to one. Zero will result in no reflections while one will travel fragment-by-fragment along the ray's direction. A `resolution` of one can slow down your FPS considerably especially with a large `maxDistance`. The `steps` parameter controls how many iterations occur during the second pass. This second pass is to find the exact point along the reflection ray's direction where the ray immediately hits or intersects with some geometry in the scene. Think of this second pass as the refinement pass. The `thickness` controls the cutoff between what counts as a possible reflection hit and what does not. Ideally, you'd like to have the ray immediately stop at some camera-captured position or depth in the scene. This would be the exact point where the light ray bounced off, hit your current fragment, and then bounced off into the camera. Unfortunately the calculations are not always that precise so `thickness` provides some wiggle room or tolerance. You'll want the thickness to be as small as possible—just a short distance beyond a sampled position or depth.

Larger Thickness

You'll find that as the thickness gets larger, the reflections tend to smear in places.

Smaller Thickness

Going in the other direction, as the thickness gets smaller, the reflections become noisy with tiny little holes and narrow gaps. ```c // ... vec2 texSize = textureSize(positionTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 positionFrom = texture(positionTexture, texCoord); vec3 unitPositionFrom = normalize(positionFrom.xyz); vec3 normal = normalize(texture(normalTexture, texCoord).xyz); vec3 pivot = normalize(reflect(unitPositionFrom, normal)); // ... ``` Gather the current fragment's position, normal, and reflection about the normal. `positionFrom` is a vector from the camera position to the current fragment position. `normal` is a vector pointing in the direction of the interpolated vertex normal for the current fragment. `pivot` is the reflection ray or vector pointing in the reflected direction of the `positionFrom` vector. It currently has a length or magnitude of one. ```c // ... vec4 startView = vec4(positionFrom.xyz + (pivot * 0), 1); vec4 endView = vec4(positionFrom.xyz + (pivot * maxDistance), 1); // ... ``` Calculate the start and end point of the reflection ray in view space. ```c // ... vec4 startFrag = startView; // Project to screen space. startFrag = lensProjection * startFrag; // Perform the perspective divide. startFrag.xyz /= startFrag.w; // Convert the screen-space XY coordinates to UV coordinates. startFrag.xy = startFrag.xy * 0.5 + 0.5; // Convert the UV coordinates to fragment/pixel coordnates. startFrag.xy *= texSize; vec4 endFrag = endView; endFrag = lensProjection * endFrag; endFrag.xyz /= endFrag.w; endFrag.xy = endFrag.xy * 0.5 + 0.5; endFrag.xy *= texSize; // ... ``` Project or transform these start and end points from view space to screen space. These points are now fragment positions which correspond to pixel positions on the screen. Now that you know where the ray starts and ends on the screen, you can travel or march along its direction in screen space. Think of the ray as a line drawn on the screen. You'll travel along this line using it to sample the fragment positions stored in the position framebuffer texture.

Screen space versus view space.

Note that you could march the ray through view space but this may under or over sample scene positions found in the position framebuffer texture. Recall that the position framebuffer texture is the size and shape of the screen. Every screen fragment or pixel corresponds to some position captured by the camera. A reflection ray may travel a long distance in view space, but in screen space, it may only travel through a few pixels. You can only sample the screen's pixels for positions so it is inefficient to potentially sample the same pixels over and over again while marching in view space. By marching in screen space, you'll more efficiently sample the fragments or pixels the ray actually occupies or covers. ```c // ... vec2 frag = startFrag.xy; uv.xy = frag / texSize; // ... ``` The first pass will begin at the starting fragment position of the reflection ray. Convert the fragment position to a UV coordinate by dividing the fragment's coordinates by the position texture's dimensions. ```c // ... float deltaX = endFrag.x - startFrag.x; float deltaY = endFrag.y - startFrag.y; // ... ``` Calculate the delta or difference between the X and Y coordinates of the end and start fragments. This will be how many pixels the ray line occupies in the X and Y dimension of the screen.

The reflection ray in screen space.

```c // ... float useX = abs(deltaX) >= abs(deltaY) ? 1 : 0; float delta = mix(abs(deltaY), abs(deltaX), useX) * clamp(resolution, 0, 1); // ... ``` To handle all of the various different ways (vertical, horizontal, diagonal, etc.) the line can be oriented, you'll need to keep track of and use the larger difference. The larger difference will help you determine how much to travel in the X and Y direction each iteration, how many iterations are needed to travel the entire line, and what percentage of the line does the current position represent. `useX` is either one or zero. It is used to pick the X or Y dimension depending on which delta is bigger. `delta` is the larger delta of the two X and Y deltas. It is used to determine how much to march in either dimension each iteration and how many iterations to take during the first pass. ```c // ... vec2 increment = vec2(deltaX, deltaY) / max(delta, 0.001); // ... ``` Calculate how much to increment the X and Y position by using the larger of the two deltas. If the two deltas are the same, each will increment by one each iteration. If one delta is larger than the other, the larger delta will increment by one while the smaller one will increment by less than one. This assumes the `resolution` is one. If the resolution is less than one, the algorithm will skip over fragments. ```c startFrag = ( 1, 4) endFrag = (10, 14) deltaX = (10 - 1) = 9 deltaY = (14 - 4) = 10 resolution = 0.5 delta = 10 * 0.5 = 5 increment = (deltaX, deltaY) / delta = ( 9, 10) / 5 = ( 9 / 5, 2) ``` For example, say the `resolution` is 0.5. The larger dimension will increment by two fragments instead of one. ```c // ... float search0 = 0; float search1 = 0; // ... ``` To move from the start fragment to the end fragment, the algorithm uses linear interpolation. ```c current position x = (start x) * (1 - search1) + (end x) * search1; current position y = (start y) * (1 - search1) + (end y) * search1; ``` `search1` ranges from zero to one. When `search1` is zero, the current position is the start fragment. When `search1` is one, the current position is the end fragment. For any other value, the current position is somewhere between the start and end fragment. `search0` is used to remember the last position on the line where the ray missed or didn't intersect with any geometry. The algorithm will later use `search0` in the second pass to help refine the point at which the ray touches the scene's geometry. ```c // ... int hit0 = 0; int hit1 = 0; // ... ``` `hit0` indicates there was an intersection during the first pass. `hit1` indicates there was an intersection during the second pass. ```c // ... float viewDistance = startView.y; float depth = thickness; // ... ``` The `viewDistance` value is how far away from the camera the current point on the ray is. Recall that for Panda3D, the Y dimension goes in and out of the screen in view space. For other systems, the Z dimension goes in and out of the screen in view space. In any case, `viewDistance` is how far away from the camera the ray currently is. Note that if you use the depth buffer, instead of the vertex positions in view space, the `viewDistance` would be the Z depth. Make sure not to confuse the `viewDistance` value with the Y dimension of the line being traveled across the screen. The `viewDistance` goes from the camera into scene while the Y dimension of the line travels up or down the screen. The `depth` is the view distance difference between the current ray point and scene position. It tells you how far behind or in front of the scene the ray currently is. Remember that the scene positions are the interpolated vertex positions stored in the position framebuffer texture. ```c // ... for (i = 0; i < int(delta); ++i) { // ... ``` You can now begin the first pass. The first pass runs while `i` is less than the `delta` value. When `i` reaches `delta`, the algorithm has traveled the entire length of the line. Remember that `delta` is the larger of the two X and Y deltas.

Screen Space Transformations

```c // ... frag += increment; uv.xy = frag / texSize; positionTo = texture(positionTexture, uv.xy); // ... ``` Advance the current fragment position closer to the end fragment. Use this new fragment position to look up a scene position stored in the position framebuffer texture. ```c // ... search1 = mix ( (frag.y - startFrag.y) / deltaY , (frag.x - startFrag.x) / deltaX , useX ); // ... ``` Calculate the percentage or portion of the line the current fragment represents. If `useX` is zero, use the Y dimension of the line. If `useX` is one, use the X dimension of the line. When `frag` equals `startFrag`, `search1` equals zero since `frag - startFrag` is zero. When `frag` equals `endFrag`, `search1` is one since `frag - startFrag` equals `delta`. `search1` is the percentage or portion of the line the current position represents. You'll need this to interpolate between the ray's view-space start and end distances from the camera. ```c // ... viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); // ... ``` Using `search1`, interpolate the view distance (distance from the camera in view space) for the current position you're at on the reflection ray. ```c // Incorrect. viewDistance = mix(startView.y, endView.y, search1); // Correct. viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); ``` You may be tempted to just interpolate between the view distances of the start and end view-space positions but this will give you the wrong view distance for the current position on the reflection ray. Instead, you'll need to perform [perspective-correct interpolation](https://www.comp.nus.edu.sg/~lowkl/publications/lowk_persp_interp_techrep.pdf) which you see here. ```c // ... depth = viewDistance - positionTo.y; // ... ``` Calculate the difference between the ray's view distance at this point and the sampled view distance of the scene at this point. ```c // ... if (depth > 0 && depth < thickness) { hit0 = 1; break; } else { search0 = search1; } // ... ``` If the difference is between zero and the thickness, this is a hit. Set `hit0` to one and exit the first pass. If the difference is not between zero and the thickness, this is a miss. Set `search0` to equal `search1` to remember this position as the last known miss. Continue marching the ray towards the end fragment. ```c // ... search1 = search0 + ((search1 - search0) / 2); // ... ``` At this point you have finished the first pass. Set the `search1` position to be halfway between the position of the last miss and the position of the last hit. ```c // ... steps *= hit0; for (i = 0; i < steps; ++i) { // ... ``` You can now begin the second pass. If the reflection ray didn't hit anything in the first pass, skip the second pass. ```c // ... frag = mix(startFrag.xy, endFrag.xy, search1); uv.xy = frag / texSize; positionTo = texture(positionTexture, uv.xy); // ... ``` As you did in the first pass, use the current position on the ray line to sample a position from the scene. ```c // ... viewDistance = (startView.y * endView.y) / mix(endView.y, startView.y, search1); depth = viewDistance - positionTo.y; // ... ``` Interpolate the view distance for the current ray line position and calculate the camera distance difference between the ray at this point and the scene. ```c // ... if (depth > 0 && depth < thickness) { hit1 = 1; search1 = search0 + ((search1 - search0) / 2); } else { float temp = search1; search1 = search1 + ((search1 - search0) / 2); search0 = temp; } // ... ``` If the depth is within bounds, this is a hit. Set `hit1` to one and set `search1` to be halfway between the last known miss position and this current hit position. If the depth is not within bounds, this is a miss. Set `search1` to be halfway between this current miss position and the last known hit position. Move `search0` to this current miss position. Continue this back and forth search while `i` is less than `steps`. ```c // ... float visibility = hit1 // ... ``` You're now done with the second and final pass but before you can output the reflected UV coordinates, you'll need to calculate the `visibility` of the reflection. The `visibility` ranges from zero to one. If there wasn't a hit in the second pass, the `visibility` is zero. ```c // ... * positionTo.w // ... ``` If the reflected scene position's alpha or `w` component is zero, the `visibility` is zero. Note that if `w` is zero, there was no scene position at that point.

Reflection ray pointing towards the camera position.

```c // ... * ( 1 - max ( dot(-unitPositionFrom, pivot) , 0 ) ) // ... ``` One of the ways in which screen space reflection can fail is when the reflection ray points in the general direction of the camera. If the reflection ray points towards the camera and hits something, it's most likely hitting the back side of something facing away from the camera. To handle this failure case, you'll need to gradually fade out the reflection based on how much the reflection vector points to the camera's position. If the reflection vector points directly in the opposite direction of the position vector, the visibility is zero. Any other direction results in the visibility being greater than zero. Remember to normalize both vectors when taking the dot product. `unitPositionFrom` is the normalized position vector. It has a length or magnitude of one. ```c // ... * ( 1 - clamp ( depth / thickness , 0 , 1 ) ) // ... ``` As you sample scene positions along the reflection ray, you're hoping to find the exact point where the reflection ray first intersects with the scene's geometry. Unfortunately, you may not find this particular point. Fade out the reflection the further it is from the intersection point you did find. ```c // ... * ( 1 - clamp ( length(positionTo - positionFrom) / maxDistance , 0 , 1 ) ) // ... ``` Fade out the reflection based on how far way the reflected point is from the initial starting point. This will fade out the reflection instead of it ending abruptly as it reaches `maxDistance`.

Reflection ray exiting the frustum.

```c // ... * (uv.x < 0 || uv.x > 1 ? 0 : 1) * (uv.y < 0 || uv.y > 1 ? 0 : 1); // ... ``` If the reflected UV coordinates are out of bounds, set the `visibility` to zero. This occurs when the reflection ray travels outside the camera's frustum. ```c visibility = clamp(visibility, 0, 1); uv.ba = vec2(visibility); ``` Set the blue and alpha component to the visibility as the UV coordinates only need the RG or XY components of the final vector. ```c // ... fragColor = uv; // ... ``` The final fragment color is the reflected UV coordinates and the visibility. ### Specular Map

Specular Map

In addition to the reflected UV coordinates, you'll also need a specular map. The example code creates one using the fragment's material specular properties. ```c // ... #define MAX_SHININESS 127.75 uniform struct { vec3 specular ; float shininess ; } p3d_Material; out vec4 fragColor; void main() { fragColor = vec4 ( p3d_Material.specular , clamp(p3d_Material.shininess / MAX_SHININESS, 0, 1) ); } ``` The specular fragment shader is quite simple. Using the fragment's material, the shader outputs the specular color and uses the alpha channel for the shininess. The shininess is mapped to a range of zero to one. In Blender, the maximum specular hardness or shininess is 511. When exporting from Blender to Panda3D, 511 is exported as 127.75. Feel free to adjust the shininess to range of zero to one however you see fit for your particular stack. The example code generates a specular map from the material specular properties but you could create one in GIMP, for example, and attach that as a texture to your 3D model. For instance, say your 3D treasure chest has shiny brackets on it but nothing else should reflect the environment. You can paint the brackets some shade of gray and the rest of the treasure chest black. This will mask off the brackets, allowing your shader to render the reflections on only the brackets and nothing else. ### Scene Colors

Scene Colors

You'll need to render the parts of the scene you wish to reflect and store this in a framebuffer texture. This is typically just the scene without any reflections. ### Reflected Scene Colors

Reflected Scene Colors

Here you see the reflected colors saved to a framebuffer texture. ```c // ... uniform sampler2D uvTexture; uniform sampler2D colorTexture; // ... ``` Once you have the reflected UV coordinates, looking up the reflected colors is fairly easy. You'll need the reflected UV coordinates texture and the color texture containing the colors you wish to reflect. ```c // ... vec2 texSize = textureSize(uvTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 uv = texture(uvTexture, texCoord); vec4 color = texture(colorTexture, uv.xy); // ... ``` Using the UV coordinates for the current fragment, look up the reflected color. ```c // ... float alpha = clamp(uv.b, 0, 1); // ... ``` Recall that the reflected UV texture stored the visibility in the B or blue component. This is the alpha channel for the reflected colors framebuffer texture. ```c // ... fragColor = vec4(mix(vec3(0), color.rgb, alpha), alpha); // ... ``` The fragment color is a mix between no reflection and the reflected color based on the visibility. The visibility was computed during the reflected UV coordinates step. ### Blurred Reflected Scene Colors

Blurred Reflected Scene Colors

Now blur the reflected scene colors and store this in a framebuffer texture. The blurring is done using a box blur. Refer to the [SSAO blurring](ssao.md#blurring) step for details. The blurred reflected colors are used for surfaces that have a less than mirror like finish. These surfaces have tiny little hills and valleys that tend to diffuse or blur the reflection. I'll cover this more during the roughness calculation. ### Reflections ```c // ... uniform sampler2D colorTexture; uniform sampler2D colorBlurTexture; uniform sampler2D specularTexture; // ... ``` To generate the final reflections, you'll need the three framebuffer textures computed earlier. You'll need the reflected colors, the blurred reflected colors, and the specular map. ```c // ... vec4 specular = texture(specularTexture, texCoord); vec4 color = texture(colorTexture, texCoord); vec4 colorBlur = texture(colorBlurTexture, texCoord); // ... ``` Look up the specular amount and shininess, the reflected scene color, and the blurred reflected scene color. ```c // ... float specularAmount = dot(specular.rgb, vec3(1)) / 3; if (specularAmount <= 0) { fragColor = vec4(0); return; } // ... ``` Map the specular color to a greyscale value. If the specular amount is none, set the frag color to nothing and return. Later on, you'll multiply the final reflection color by the specular amount. Multiplying by the specular amount allows you to control how much a material reflects its environment simply by brightening or darkening the greyscale value in the specular map. ```c dot(specular.rgb, vec3(1)) == (specular.r + specular.g + specular.b); ``` Using the dot product to produce the greyscale value is just a short way of summing the three color components. ```c // ... float roughness = 1 - min(specular.a, 1); // ... ``` Calculate the roughness based on the shininess value set during the specular map step. Recall that the shininess value was saved in the alpha channel of the specular map. The shininess determined how spread out or blurred the specular reflection was. Similarly, the `roughness` determines how blurred the reflection is. A roughness of one will produce the blurred reflection color. A roughness of zero will produce the non-blurred reflection color. Doing it this way allows you to control how blurred the reflection is just by changing the material's shininess value. The example code generates a roughness map from the material specular properties but you could create one in GIMP, for example, and attach that as a texture to your 3D model. For instance, say you have a tiled floor that has polished tiles and scratched up tiles. The polished tiles could be painted a more translucent white while the scratched up tiles could be painted a more opaque white. The more translucent/transparent the greyscale value, the more the shader will use the blurred reflected color. The scratched tiles will have a blurry reflection while the polished tiles will have a mirror like reflection. ```c // ... fragColor = mix(color, colorBlur, roughness) * specularAmount; // ... ``` Mix the reflected color and blurred reflected color based on the roughness. Multiply that vector by the specular amount and then set that value as the fragment color. The reflection color is a mix between the reflected scene color and the blurred reflected scene color based on the roughness. A high roughness will produce a blurry reflection meaning the surface is rough. A low roughness will produce a clear reflection meaning the surface is smooth. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [normal.frag](../demonstration/shaders/fragment/normal.frag) - [material-specular.frag](../demonstration/shaders/fragment/material-specular.frag) - [screen-space-reflection.frag](../demonstration/shaders/fragment/screen-space-reflection.frag) - [reflection-color.frag](../demonstration/shaders/fragment/reflection-color.frag) - [reflection.frag](../demonstration/shaders/fragment/reflection.frag) - [box-blur.frag](../demonstration/shaders/fragment/box-blur.frag) - [base-combine.frag](../demonstration/shaders/fragment/base-combine.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](chromatic-aberration.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](screen-space-refraction.md) ================================================ FILE: sections/screen-space-refraction.md ================================================ [:arrow_backward:](screen-space-reflection.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](foam.md) # 3D Game Shaders For Beginners ## Screen Space Refraction (SSR)

Screen Space Refraction

Screen space refraction, much like [screen space reflection](screen-space-reflection.md), adds a touch of realism you can't find anywhere else. Glass, plastic, water, and other transparent/translucent materials spring to life. [Screen space reflection](screen-space-reflection.md) and screen space refraction work almost identically expect for one major difference. Instead of using the reflected vector, screen space refraction uses the [refracted vector](http://asawicki.info/news_1301_reflect_and_refract_functions.html). It's a slight change in code but a big difference visually. ### Vertex Positions Like SSAO, you'll need the vertex positions in view space. Referrer back to [SSAO](ssao.md#vertex-positions) for details. However, unlike SSAO, you'll need the scene's vertex positions with and without the refractive objects. Refractive surfaces are translucent, meaning you can see through them. Since you can see through them, you'll need the vertex positions behind the refractive surface. Having both the foreground and background vertex positions will allow you to calculate UV coordinates and depth. ### Vertex Normals To compute the refractions, you'll need the scene's foreground vertex normals in view space. The background vertex normals aren't needed unless you need to incorporate the background surface detail while calculating the refracted UV coordinates and distances. Referrer back to [SSAO](ssao.md#vertex-normals) for details.

Normal maps versus no normal maps.

Here you see the water refracting the light with and without normal maps. If available, be sure to use the normal mapped normals instead of the vertex normals. The smoother and flatter the surface, the harder it is to tell the light is being refracted. There will be some distortion but not enough to make it worthwhile. To use the normal maps instead, you'll need to transform the normal mapped normals from tangent space to view space just like you did in the [lighting](normal-mapping.md#fragment) calculations. You can see this being done in [normal.frag](../demonstration/shaders/fragment/normal.frag). ### Position Transformations

Position Transformations

Just like [SSAO](ssao.md) and [screen space reflection](screen-space-reflection.md), screen space refraction goes back and forth between the screen and view space. You'll need the camera lens' projection matrix to transform points in view space to clip space. From clip space, you'll have to transform the points again to UV space. Once in UV space, you can sample a vertex/fragment position from the scene which will be the closest position in the scene to your sample. This is the _screen space_ part in _screen space refraction_ since the "screen" is a texture UV mapped over a screen shaped rectangle. ### Refracted UV Coordinates Recall that UV coordinates range from zero to one for both U and V. The screen is just a 2D texture UV mapped over a screen-sized rectangle. Knowing this, the example code doesn't actually need the final rendering of the scene to compute the refraction. It can instead calculate what UV coordinate each screen pixel will eventually use. These calculated UV coordinates can be saved to a framebuffer texture and used later when the scene has been rendered. The process of refracting the UV coordinates is very similar to the process of [reflecting the UV coordinates](screen-space-reflection.md#reflected-uv-coordinates). Below are the adjustments you'll need to turn reflection into refraction. ```c // ... uniform sampler2D positionFromTexture; uniform sampler2D positionToTexture; uniform sampler2D normalFromTexture; // ... ``` Reflection only deals with what is in front of the reflective surface. Refraction, however, deals with what is behind the refractive surface. To accommodate this, you'll need both the vertex positions of the scene with the refracting surfaces taken out and the vertex positions of the scene with the refracting surfaces left in.

Without and with refractive surfaces.

`positionFromTexture` are the scene's vertex positions with the refracting surfaces left in. `positionToTexture` are the scene's vertex positions with the refracting surfaces taken out. `normalFromTexture` are the scene's vertex normals with the refraction surfaces left in. There's no need for the vertex normals behind the refractive surfaces unless you want to incorporate the surface detail for the background geometry. ```c // ... uniform vec2 rior; // ... ``` Refraction has one more adjustable parameter than reflection. `rior` is the relative index of refraction or relative refractive index. It is the ratio of the refraction indexes of two mediums. So for example, going from water to air is `1 / 1.33 ≈ 0.75`. The numerator is the refractive index of the medium the light is leaving and the denominator is the refractive index of the medium the light is entering. An `rior` of one means the light passes right through without being refracted or bent. As `rior` grows, the refraction will become a [reflection](https://en.wikipedia.org/wiki/Total_internal_reflection). There's no requirement that `rior` must adhere to the real world. The demo uses `1.05`. This is completely unrealistic (light does not travel faster through water than air) but the realistic setting produced too many artifacts. In the end, the distortion only has to be believable—not realistic.

Adjusting the relative index of refraction.

`rior` values above one tend to elongate the refraction while numbers below one tend to shrink the refraction. As it was with screen space reflection, the screen doesn't have the entire geometry of the scene. A refracted ray may march through the screen space and never hit a captured surface. Or it may hit a surface but it's the backside not captured by the camera. When this happened during reflection, the fragment was left blank. This indicated no reflection or not enough information to determine a reflection. Leaving the fragment blank was fine for reflection since the reflective surface would fill in the gaps.

Refraction Holes

For refraction, however, we must set the fragment to some UV. If the fragment is left blank, the refractive surface will contain holes that let the detail behind it come through. This would be okay for a completely transparent surface but usually the refractive surface will have some tint to it, reflection, etc. ```c // ... vec2 texSize = textureSize(positionFromTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 uv = vec4(texCoord.xy, 1, 1); // ... ``` The best choice is to select the UV as if the `rior` was one. This will leave the UV coordinate unchanged, allowing the background to show through instead of there being a hole in the refractive surface.

Refraction UV Map

Here you see the refracted UV texture for the mill scene. The wheel and waterway disturb what is otherwise a smooth gradient. The disruptions shift the UV coordinates from their screen position to their refracted screen position. ```c // ... vec3 unitPositionFrom = normalize(positionFrom.xyz); vec3 normalFrom = normalize(texture(normalFromTexture, texCoord).xyz); vec3 pivot = normalize(refract(unitPositionFrom, normalFrom, rior.x)); // ... ``` The most important difference is the calculation of the refracted vector versus the reflected vector. Both use the unit position and normal but `refract` takes an additional parameter specifying the relative refractive index. ```c // ... frag += increment; uv.xy = frag / texSize; positionTo = texture(positionToTexture, uv.xy); // ... ``` The `positionTo`, sampled by the `uv` coordinates, uses the `positionToTexture`. For reflection, you only need one framebuffer texture containing the scene's interpolated vertex positions in view space. However, for refraction, `positionToTexture` contains the vertex positions of the scene minus the refractive surfaces since the refraction ray typically goes behind the surface. If `positionFromTexture` and `positionToTexture` were the same for refraction, the refracted ray would hit the refractive surface instead of what is behind it. ### Refraction Mask

Material Specular

You'll need a mask to filter out the non-refractive parts of the scene. This mask will determine which fragment does and does not receive a refracted color. You could use this mask during the refracted UV calculation step or later when you actually sample the colors at the refracted UV coordinates. The mill scene uses the models' material specular as a mask. For the demo's purposes, the specular map is sufficient but you may want to use a more specialized map. Refer back to [screen space reflection](screen-space-reflection.md#specular-map) for how to render the specular map. ## Background Colors

Background Colors

You'll need to render the parts of the scene behind the refractive objects. This can be done by hiding the refractive objects and then rendering the scene to a framebuffer texture. ### Foreground Colors

Foreground Colors

```c // ... uniform sampler2D uvTexture; uniform sampler2D refractionMaskTexture; uniform sampler2D positionFromTexture; uniform sampler2D positionToTexture; uniform sampler2D backgroundColorTexture; // ... ``` To render the actual refractions or foreground colors, you'll need the refracted UV coordinates, refraction mask, the foreground and background vertex positions, and the background colors. ```c // ... vec3 tintColor = vec3(0.27, 0.58, 0.92, 0.3); float depthMax = 2; // ... ``` `tintColor` and `depthMax` are adjustable parameters. `tintColor` colorizes the background color. `depthMax` ranges from zero to infinity. When the distance between the foreground and background position reaches `depthMax`, the foreground color will be the fully tinted background color. At distance zero, the foreground will be the background color. ```c // ... vec2 texSize = textureSize(backgroundColorTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 uv = texture(uvTexture, texCoord); vec4 mask = texture(maskTexture, texCoord); vec4 positionFrom = texture(positionFromTexture, texCoord); vec4 positionTo = texture(positionToTexture, uv.xy); vec4 backgroundColor = texture(backgroundColorTexture, uv.xy); if (refractionMask.r <= 0) { fragColor = vec4(0); return; } // ... ``` Pull out the uv coordinates, mask, background position, foreground position, and the background color. If the refraction mask is turned off for this fragment, return nothing. ```c // ... float depth = length(positionTo.xyz - positionFrom.xyz); float mixture = clamp(depth / depthMax, 0, 1); vec3 shallowColor = backgroundColor.rgb; vec3 deepColor = mix(shallowColor, tintColor.rgb, tintColor.a); vec3 foregroundColor = mix(shallowColor, deepColor, mixture); // ... ```

Refraction Depth

Calculate the depth or distance between the foreground position and the background position. At zero depth, the foreground color will be the shallow color. At `depthMax`, the foreground color will be the deep color. The deep color is the background color tinted with `tintColor`. ```c // ... fragColor = mix(vec4(0), vec4(foregroundColor, 1), uv.b); // ... ``` Recall that the blue channel, in the refracted UV texture, is set to the visibility. The visibility declines as the refracted ray points back at the camera. While the visibility should always be one, it is put here for completeness. As the visibility lessens, the fragment color will receive less and less of the foreground color. ### Source - [main.cxx](../demonstration/src/main.cxx) - [base.vert](../demonstration/shaders/vertex/base.vert) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [position.frag](../demonstration/shaders/fragment/position.frag) - [normal.frag](../demonstration/shaders/fragment/normal.frag) - [material-specular.frag](../demonstration/shaders/fragment/material-specular.frag) - [screen-space-refraction.frag](../demonstration/shaders/fragment/screen-space-refraction.frag) - [refraction.frag](../demonstration/shaders/fragment/refraction.frag) - [base-combine.frag](../demonstration/shaders/fragment/base-combine.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](screen-space-reflection.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](foam.md) ================================================ FILE: sections/setup.md ================================================ [:arrow_backward:](gamma-correction.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](building-the-demo.md) # 3D Game Shaders For Beginners ## Setup

Setup

Below is the setup used to develop and test the example code. ### Environment The example code was developed and tested using the following environment. - Linux manjaro 5.10.42-1-MANJARO - OpenGL renderer string: GeForce GTX 970/PCIe/SSE2 - OpenGL version string: 4.6.0 NVIDIA 465.31 - g++ (GCC) 11.1.0 - Panda3D 1.10.9 ### Materials Each [Blender](https://blender.org) material used to build `mill-scene.egg` has five textures in the following order. - Diffuse - Normal - Specular - Reflection - Refraction By having the same maps in the same positions for all models, the shaders can be generalized, reducing the need to duplicate code. If an object uses its vertex normals, a "flat blue" normal map is used.

A flat normal map.

Here is an example of a flat normal map. The only color it contains is flat blue `(red = 128, green = 128, blue = 255)`. This color represents a unit (length one) normal pointing in the positive z-axis `(0, 0, 1)`. ```c (0, 0, 1) = ( round((0 * 0.5 + 0.5) * 255) , round((0 * 0.5 + 0.5) * 255) , round((1 * 0.5 + 0.5) * 255) ) = (128, 128, 255) = ( round(128 / 255 * 2 - 1) , round(128 / 255 * 2 - 1) , round(255 / 255 * 2 - 1) ) = (0, 0, 1) ``` Here you see the unit normal `(0, 0, 1)` converted to flat blue `(128, 128, 255)` and flat blue converted to the unit normal. You'll learn more about this in the [normal mapping](normal-mapping.md) technique.

Specular Map

Up above is one of the specular maps used. The red and blue channel work to control the amount of specular reflection seen based on the camera angle. The green channel controls the shininess factor. You'll learn more about this in the [lighting](lighting.md) and [fresnel factor](fresnel-factor.md) sections. The reflection and refraction textures mask off the objects that are either reflective, refractive, or both. For the reflection texture, the red channel controls the amount of reflection and the green channel controls how clear or blurry the reflection is. ### Panda3D The example code uses [Panda3D](https://www.panda3d.org/) as the glue between the shaders. This has no real influence over the techniques described, meaning you'll be able to take what you learn here and apply it to your stack or game engine of choice. Panda3D does provide some conveniences. I have pointed these out so you can either find an equivalent convenience provided by your stack or replicate it yourself, if your stack doesn't provide something equivalent. Three Panda3D configurations were changed for the purposes of the demo program. You can find these in [config.prc](../demonstration/config.prc). The configurations changed were `gl-coordinate-system default`, `textures-power-2 down`, and `textures-auto-power-2 1`. Refer to the [Panda3D configuration](http://www.panda3d.org/manual/?title=Configuring_Panda3D) page in the manual for more details. Panda3D defaults to a z-up, right-handed coordinate system while OpenGL uses a y-up, right-handed system. `gl-coordinate-system default` keeps you from having to translate between the two inside your shaders. `textures-auto-power-2 1` allows us to use texture sizes that are not a power of two if the system supports it. This comes in handy when doing SSAO and other screen/window sized related techniques since the screen/window size is usually not a power of two. `textures-power-2 down` downsizes our textures to a power of two if the system only supports texture sizes being a power of two. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](gamma-correction.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](building-the-demo.md) ================================================ FILE: sections/sharpen.md ================================================ [:arrow_backward:](pixelization.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](dilation.md) # 3D Game Shaders For Beginners ## Sharpen

Sharpen

The sharpen effect increases the contrast at the edges of the image. This comes in handy when your graphics are bit too soft. ```c // ... float amount = 0.8; // ... ``` You can control how sharp the result is by adjusting the amount. An amount of zero leaves the image untouched. Try negative values for an odd look. ```c // ... float neighbor = amount * -1; float center = amount * 4 + 1; // ... ``` Neighboring fragments are multiplied by `amount * -1`. The current fragment is multiplied by `amount * 4 + 1`. ```c // ... vec3 color = texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 1) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x - 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 0) / texSize).rgb * center + texture(sharpenTexture, vec2(gl_FragCoord.x + 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y - 1) / texSize).rgb * neighbor ; // ... ``` The neighboring fragments are up, down, left, and right. After multiplying both the neighbors and the current fragment by their particular values, sum the result. ```c // ... fragColor = vec4(color, texture(sharpenTexture, texCoord).a); // ... ``` This sum is the final fragment color. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [sharpen.frag](../demonstration/shaders/fragment/sharpen.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](pixelization.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](dilation.md) ================================================ FILE: sections/ssao.md ================================================ [:arrow_backward:](bloom.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](motion-blur.md) # 3D Game Shaders For Beginners ## Screen Space Ambient Occlusion (SSAO)

SSAO

SSAO is one of those effects you never knew you needed and can't live without once you have it. It can take a scene from mediocre to wow! For fairly static scenes, you can bake ambient occlusion into a texture but for more dynamic scenes, you'll need a shader. SSAO is one of the more fairly involved shading techniques, but once you pull it off, you'll feel like a shader master. By using only a handful of textures, SSAO can approximate the [ambient occlusion](https://en.wikipedia.org/wiki/Ambient_occlusion) of a scene. This is faster than trying to compute the ambient occlusion by going through all of the scene's geometry. These handful of textures all originate in screen space giving screen space ambient occlusion its name. ### Inputs The SSAO shader will need the following inputs. - Vertex position vectors in view space. - Vertex normal vectors in view space. - Sample vectors in tangent space. - Noise vectors in tangent space. - The camera lens' projection matrix. ### Vertex Positions

Panda3D Vertex Positions

Storing the vertex positions into a framebuffer texture is not a necessity. You can recreate them from the [camera's depth buffer](http://theorangeduck.com/page/pure-depth-ssao). This being a beginners guide, I'll avoid this optimization and keep it straight forward. Feel free to use the depth buffer, however, for your implementation. ```cpp PT(Texture) depthTexture = new Texture("depthTexture"); depthTexture->set_format ( Texture::Format::F_depth_component32 ); PT(GraphicsOutput) depthBuffer = graphicsOutput->make_texture_buffer ( "depthBuffer" , 0 , 0 , depthTexture ); depthBuffer->set_clear_color ( LVecBase4f(0, 0, 0, 0) ); NodePath depthCameraNP = window->make_camera(); DCAST(Camera, depthCameraNP.node())->set_lens ( window->get_camera(0)->get_lens() ); PT(DisplayRegion) depthBufferRegion = depthBuffer->make_display_region ( 0 , 1 , 0 , 1 ); depthBufferRegion->set_camera(depthCameraNP); ``` If you do decide to use the depth buffer, here's how you can set it up using Panda3D. ```c in vec4 vertexPosition; out vec4 fragColor; void main() { fragColor = vertexPosition; } ``` Here's the simple shader used to render out the view space vertex positions into a framebuffer texture. The more involved work is setting up the framebuffer texture such that the fragment vector components it receives are not clamped to `[0, 1]` and that each one has a high enough precision (a high enough number of bits). For example, if a particular interpolated vertex position is `<-139.444444566, 0.00000034343, 2.5>`, you don't want it stored into the texture as `<0.0, 0.0, 1.0>`. ```c // ... FrameBufferProperties fbp = FrameBufferProperties::get_default(); // ... fbp.set_rgba_bits(32, 32, 32, 32); fbp.set_rgb_color(true); fbp.set_float_color(true); // ... ``` Here's how the example code sets up the framebuffer texture to store the vertex positions. It wants 32 bits per red, green, blue, and alpha components and disables clamping the values to `[0, 1]` The `set_rgba_bits(32, 32, 32, 32)` call sets the bits and also disables the clamping. ```c glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGB32F , 1200 , 900 , 0 , GL_RGB , GL_FLOAT , nullptr ); ``` Here's the equivalent OpenGL call. `GL_RGB32F` sets the bits and also disables the clamping.
If the color buffer is fixed-point, the components of the source and destination values and blend factors are each clamped to [0, 1] or [−1, 1] respectively for an unsigned normalized or signed normalized color buffer prior to evaluating the blend equation. If the color buffer is floating-point, no clamping occurs.

OpenGL Vertex Positions

Here you see the vertex positions with y being the up vector. Recall that Panda3D sets z as the up vector but OpenGL uses y as the up vector. The position shader outputs the vertex positions with z being up since Panda3D was configured with `gl-coordinate-system default`. ### Vertex Normals

Panda3d Vertex Normals

You'll need the vertex normals to correctly orient the samples you'll take in the SSAO shader. The example code generates multiple sample vectors distributed in a hemisphere but you could use a sphere and do away with the need for normals all together. ```c in vec3 vertexNormal; out vec4 fragColor; void main() { vec3 normal = normalize(vertexNormal); fragColor = vec4(normal, 1); } ``` Like the position shader, the normal shader is simple as well. Be sure to normalize the vertex normal and remember that they are in view space.

OpenGL Vertex Normals

Here you see the vertex normals with y being the up vector. Recall that Panda3D sets z as the up vector but OpenGL uses y as the up vector. The normal shader outputs the vertex positions with z being up since Panda3D was configured with `gl-coordinate-system default`.

SSAO using the normal maps.

Here you see SSAO being used with the normal maps instead of the vertex normals. This adds an extra level of detail and will pair nicely with the normal mapped lighting. ```c // ... normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); // ... ``` To use the normal maps instead, you'll need to transform the normal mapped normals from tangent space to view space just like you did in the lighting calculations. ### Samples To determine the amount of ambient occlusion for any particular fragment, you'll need to sample the surrounding area. The more samples you use, the better the approximation at the cost of performance. ```cpp // ... for (int i = 0; i < numberOfSamples; ++i) { LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) ).normalized(); float rand = randomFloats(generator); sample[0] *= rand; sample[1] *= rand; sample[2] *= rand; float scale = (float) i / (float) numberOfSamples; scale = lerp(0.1, 1.0, scale * scale); sample[0] *= scale; sample[1] *= scale; sample[2] *= scale; ssaoSamples.push_back(sample); } // ... ``` The example code generates a number of random samples distributed in a hemisphere. These `ssaoSamples` will be sent to the SSAO shader. ```cpp LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 ).normalized(); ``` If you'd like to distribute your samples in a sphere instead, change the random `z` component to range from negative one to one. ### Noise ```c // ... for (int i = 0; i < numberOfNoise; ++i) { LVecBase3f noise = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , 0.0 ); ssaoNoise.push_back(noise); } // ... ``` To get a good sweep of the sampled area, you'll need to generate some noise vectors. These noise vectors will randomly tilt the hemisphere around the current fragment. ### Ambient Occlusion

SSAO Texture

SSAO works by sampling the view space around a fragment. The more samples that are below a surface, the darker the fragment color. These samples are positioned at the fragment and pointed in the general direction of the vertex normal. Each sample is used to look up a position in the position framebuffer texture. The position returned is compared to the sample. If the sample is farther away from the camera than the position, the sample counts towards the fragment being occluded.

SSAO Sampling

Here you see the space above the surface being sampled for occlusion. ```c // ... float radius = 1; float bias = 0.01; float magnitude = 1.5; float contrast = 1.5; // ... ``` Like some of the other techniques, the SSAO shader has a few control knobs you can tweak to get the exact look you're going for. The `bias` adds to the sample's distance from the camera. You can use the bias to combat "acne". The `radius` increases or decreases the coverage area of the sample space. The `magnitude` either lightens or darkens the occlusion map. The `contrast` either washes out or increases the starkness of the occlusion map. ```c // ... vec4 position = texture(positionTexture, texCoord); vec3 normal = normalize(texture(normalTexture, texCoord).xyz); int noiseX = int(gl_FragCoord.x - 0.5) % 4; int noiseY = int(gl_FragCoord.y - 0.5) % 4; vec3 random = noise[noiseX + (noiseY * 4)]; // ... ``` Retrieve the position, normal, and random vector for later use. Recall that the example code created a set number of random vectors. The random vector is chosen based on the current fragment's screen position. ```c // ... vec3 tangent = normalize(random - normal * dot(random, normal)); vec3 binormal = cross(normal, tangent); mat3 tbn = mat3(tangent, binormal, normal); // ... ``` Using the random and normal vectors, assemble the tangent, binormal, and normal matrix. You'll need this matrix to transform the sample vectors from tangent space to view space. ```c // ... float occlusion = NUM_SAMPLES; for (int i = 0; i < NUM_SAMPLES; ++i) { // ... } // ... ``` With the matrix in hand, the shader can now loop through the samples, subtracting how many are not occluded. ```c // ... vec3 samplePosition = tbn * samples[i]; samplePosition = position.xyz + samplePosition * radius; // ... ``` Using the matrix, position the sample near the vertex/fragment position and scale it by the radius. ```c // ... vec4 offsetUV = vec4(samplePosition, 1.0); offsetUV = lensProjection * offsetUV; offsetUV.xyz /= offsetUV.w; offsetUV.xy = offsetUV.xy * 0.5 + 0.5; // ... ``` Using the sample's position in view space, transform it from view space to clip space to UV space. ```c -1 * 0.5 + 0.5 = 0 1 * 0.5 + 0.5 = 1 ``` Recall that clip space components range from negative one to one and that UV coordinates range from zero to one. To transform clip space coordinates to UV coordinates, multiply by one half and add one half. ```c // ... vec4 offsetPosition = texture(positionTexture, offsetUV.xy); float occluded = 0; if (samplePosition.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; } // ... ``` Using the offset UV coordinates, created by projecting the 3D sample onto the 2D position texture, find the corresponding position vector. This takes you from view space to clip space to UV space back to view space. The shader takes this round trip to find out if some geometry is behind, at, or in front of this sample. If the sample is in front of or at some geometry, this sample doesn't count towards the fragment being occluded. If the sample is behind some geometry, this sample counts towards the fragment being occluded. ```c // ... float intensity = smoothstep ( 0.0 , 1.0 , radius / abs(position.y - offsetPosition.y) ); occluded *= intensity; occlusion -= occluded; // ... ``` Now weight this sampled position by how far it is inside or outside the radius. Finally, subtract this sample from the occlusion factor since it assumes all of the samples are occluded before the loop. ```c // ... occlusion /= NUM_SAMPLES; // ... fragColor = vec4(vec3(occlusion), position.a); // ... ``` Divide the occluded count by the number of samples to scale the occlusion factor from `[0, NUM_SAMPLES]` to `[0, 1]`. Zero means full occlusion and one means no occlusion. Now assign the occlusion factor to the fragment's color and you're done. ```c // ... fragColor = vec4(vec3(occlusion), position.a); // ... ``` For the demo's purposes, the example code sets the alpha channel to alpha channel of the position framebuffer texture to avoid covering up the background. ### Blurring

SSAO Blur Texture

The SSAO framebuffer texture is noisy as is. You'll want to blur it to remove the noise. Refer back to the section on [blurring](blur.md). For the best results, use a median or Kuwahara filter to preserve the sharp edges. ### Ambient Color ```c // ... vec2 ssaoBlurTexSize = textureSize(ssaoBlurTexture, 0).xy; vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize; float ssao = texture(ssaoBlurTexture, ssaoBlurTexCoord).r; vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex * ssao; // ... ``` The final stop for SSAO is back in the lighting calculation. Here you see the occlusion factor being looked up in the SSAO framebuffer texture and then included in the ambient light calculation. ### Source - [main.cxx](../demonstration/src/main.cxx) - [basic.vert](../demonstration/shaders/vertex/basic.vert) - [base.vert](../demonstration/shaders/vertex/base.vert) - [base.frag](../demonstration/shaders/fragment/base.frag) - [position.frag](../demonstration/shaders/fragment/position.frag) - [normal.frag](../demonstration/shaders/fragment/normal.frag) - [ssao.frag](../demonstration/shaders/fragment/ssao.frag) - [median-filter.frag](../demonstration/shaders/fragment/median-filter.frag) - [kuwahara-filter.frag](../demonstration/shaders/fragment/kuwahara-filter.frag) ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](bloom.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](motion-blur.md) ================================================ FILE: sections/texturing.md ================================================ [:arrow_backward:](render-to-texture.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](lighting.md) # 3D Game Shaders For Beginners ## Texturing

Diffuse Texture Only

Texturing involves mapping some color or some other kind of vector to a fragment using UV coordinates. Both U and V range from zero to one. Each vertex gets a UV coordinate and this is outputted in the vertex shader.

UV Interpolation

The fragment shader receives the UV coordinate interpolated. Interpolated meaning the UV coordinate for the fragment is somewhere between the UV coordinates for the vertexes that make up the triangle face. ### Vertex ```c #version 150 uniform mat4 p3d_ModelViewProjectionMatrix; in vec2 p3d_MultiTexCoord0; in vec4 p3d_Vertex; out vec2 texCoord; void main() { texCoord = p3d_MultiTexCoord0; gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } ``` Here you see the vertex shader outputting the texture coordinate to the fragment shader. Notice how it's a two dimensional vector. One dimension for U and one for V. ### Fragment ```c #version 150 uniform sampler2D p3d_Texture0; in vec2 texCoord; out vec2 fragColor; void main() { texColor = texture(p3d_Texture0, texCoord); fragColor = texColor; } ``` Here you see the fragment shader looking up the color at its UV coordinate and outputting that as the fragment color. #### Screen Filled Texture ```c #version 150 uniform sampler2D screenSizedTexture; out vec2 fragColor; void main() { vec2 texSize = textureSize(texture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; texColor = texture(screenSizedTexture, texCoord); fragColor = texColor; } ``` When performing render to texture, the mesh is a flat rectangle with the same aspect ratio as the screen. Because of this, you can calculate the UV coordinates knowing only A) the width and height of the screen sized texture being UV mapped to the rectangle and B) the fragment's x and y coordinate. To map x to U, divide x by the width of the input texture. Similarly, to map y to V, divide y by the height of the input texture. You'll see this technique used in the example code. ## Copyright (C) 2019 David Lettier
[lettier.com](https://www.lettier.com) [:arrow_backward:](render-to-texture.md) [:arrow_double_up:](../README.md) [:arrow_up_small:](#) [:arrow_down_small:](#copyright) [:arrow_forward:](lighting.md)