diff --git a/examples/Makefile b/examples/Makefile index 11669e3a3..235deda28 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -560,6 +560,7 @@ SHADERS = \ shaders/shaders_palette_switch \ shaders/shaders_postprocessing \ shaders/shaders_raymarching \ + shaders/shaders_shadowmap \ shaders/shaders_shapes_textures \ shaders/shaders_simple_mask \ shaders/shaders_spotlight \ diff --git a/examples/Makefile.Web b/examples/Makefile.Web index a3c2bb729..8bec06452 100644 --- a/examples/Makefile.Web +++ b/examples/Makefile.Web @@ -466,6 +466,7 @@ SHADERS = \ shaders/shaders_palette_switch \ shaders/shaders_postprocessing \ shaders/shaders_raymarching \ + shaders/shaders_shadowmap \ shaders/shaders_shapes_textures \ shaders/shaders_simple_mask \ shaders/shaders_spotlight \ diff --git a/examples/shaders/resources/models/robot.glb b/examples/shaders/resources/models/robot.glb new file mode 100644 index 000000000..549011e75 Binary files /dev/null and b/examples/shaders/resources/models/robot.glb differ diff --git a/examples/shaders/resources/shaders/glsl120/shadowmap.fs b/examples/shaders/resources/shaders/glsl120/shadowmap.fs new file mode 100644 index 000000000..668fdeb4b --- /dev/null +++ b/examples/shaders/resources/shaders/glsl120/shadowmap.fs @@ -0,0 +1,86 @@ +#version 120 + +precision mediump float; + +// This shader is based on the basic lighting shader +// This only supports one light, which is directional, and it (of course) supports shadows + +// Input vertex attributes (from vertex shader) +varying in vec3 fragPosition; +varying in vec2 fragTexCoord; +//varying in vec4 fragColor; +varying in vec3 fragNormal; + +// Input uniform values +uniform sampler2D texture0; +uniform vec4 colDiffuse; + +// Input lighting values +uniform vec3 lightDir; +uniform vec4 lightColor; +uniform vec4 ambient; +uniform vec3 viewPos; + +// Input shadowmapping values +uniform mat4 lightVP; // Light source view-projection matrix +uniform sampler2D shadowMap; + +uniform int shadowMapResolution; + +void main() +{ + // Texel color fetching from texture sampler + vec4 texelColor = texture2D(texture0, fragTexCoord); + vec3 lightDot = vec3(0.0); + vec3 normal = normalize(fragNormal); + vec3 viewD = normalize(viewPos - fragPosition); + vec3 specular = vec3(0.0); + + vec3 l = -lightDir; + + float NdotL = max(dot(normal, l), 0.0); + lightDot += lightColor.rgb*NdotL; + + float specCo = 0.0; + if (NdotL > 0.0) specCo = pow(max(0.0, dot(viewD, reflect(-(l), normal))), 16.0); // 16 refers to shine + specular += specCo; + + vec4 finalColor = (texelColor*((colDiffuse + vec4(specular, 1.0))*vec4(lightDot, 1.0))); + + // Shadow calculations + vec4 fragPosLightSpace = lightVP * vec4(fragPosition, 1); + fragPosLightSpace.xyz /= fragPosLightSpace.w; // Perform the perspective division + fragPosLightSpace.xyz = (fragPosLightSpace.xyz + 1.0f) / 2.0f; // Transform from [-1, 1] range to [0, 1] range + vec2 sampleCoords = fragPosLightSpace.xy; + float curDepth = fragPosLightSpace.z; + // Slope-scale depth bias: depth biasing reduces "shadow acne" artifacts, where dark stripes appear all over the scene. + // The solution is adding a small bias to the depth + // In this case, the bias is proportional to the slope of the surface, relative to the light + float bias = max(0.0008 * (1.0 - dot(normal, l)), 0.00008); + int shadowCounter = 0; + const int numSamples = 9; + // PCF (percentage-closer filtering) algorithm: + // Instead of testing if just one point is closer to the current point, + // we test the surrounding points as well. + // This blurs shadow edges, hiding aliasing artifacts. + vec2 texelSize = vec2(1.0f / float(shadowMapResolution)); + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float sampleDepth = texture2D(shadowMap, sampleCoords + texelSize * vec2(x, y)).r; + if (curDepth - bias > sampleDepth) + { + shadowCounter++; + } + } + } + finalColor = mix(finalColor, vec4(0, 0, 0, 1), float(shadowCounter) / float(numSamples)); + + // Add ambient lighting whether in shadow or not + finalColor += texelColor*(ambient/10.0)*colDiffuse; + + // Gamma correction + finalColor = pow(finalColor, vec4(1.0/2.2)); + gl_FragColor = finalColor; +} diff --git a/examples/shaders/resources/shaders/glsl120/shadowmap.vs b/examples/shaders/resources/shaders/glsl120/shadowmap.vs new file mode 100644 index 000000000..bace1d70d --- /dev/null +++ b/examples/shaders/resources/shaders/glsl120/shadowmap.vs @@ -0,0 +1,32 @@ +#version 120 + +// Input vertex attributes +attribute vec3 vertexPosition; +attribute vec2 vertexTexCoord; +attribute vec3 vertexNormal; +attribute vec4 vertexColor; + +// Input uniform values +uniform mat4 mvp; +uniform mat4 matModel; +uniform mat4 matNormal; + +// Output vertex attributes (to fragment shader) +varying vec3 fragPosition; +varying vec2 fragTexCoord; +varying vec4 fragColor; +varying vec3 fragNormal; + +// NOTE: Add here your custom variables + +void main() +{ + // Send vertex attributes to fragment shader + fragPosition = vec3(matModel*vec4(vertexPosition, 1.0)); + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + fragNormal = normalize(vec3(matNormal*vec4(vertexNormal, 1.0))); + + // Calculate final vertex position + gl_Position = mvp*vec4(vertexPosition, 1.0); +} diff --git a/examples/shaders/resources/shaders/glsl330/shadowmap.fs b/examples/shaders/resources/shaders/glsl330/shadowmap.fs new file mode 100644 index 000000000..506b51a8b --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/shadowmap.fs @@ -0,0 +1,86 @@ +#version 330 + +// This shader is based on the basic lighting shader +// This only supports one light, which is directional, and it (of course) supports shadows + +// Input vertex attributes (from vertex shader) +in vec3 fragPosition; +in vec2 fragTexCoord; +//in vec4 fragColor; +in vec3 fragNormal; + +// Input uniform values +uniform sampler2D texture0; +uniform vec4 colDiffuse; + +// Output fragment color +out vec4 finalColor; + +// Input lighting values +uniform vec3 lightDir; +uniform vec4 lightColor; +uniform vec4 ambient; +uniform vec3 viewPos; + +// Input shadowmapping values +uniform mat4 lightVP; // Light source view-projection matrix +uniform sampler2D shadowMap; + +uniform int shadowMapResolution; + +void main() +{ + // Texel color fetching from texture sampler + vec4 texelColor = texture(texture0, fragTexCoord); + vec3 lightDot = vec3(0.0); + vec3 normal = normalize(fragNormal); + vec3 viewD = normalize(viewPos - fragPosition); + vec3 specular = vec3(0.0); + + vec3 l = -lightDir; + + float NdotL = max(dot(normal, l), 0.0); + lightDot += lightColor.rgb*NdotL; + + float specCo = 0.0; + if (NdotL > 0.0) specCo = pow(max(0.0, dot(viewD, reflect(-(l), normal))), 16.0); // 16 refers to shine + specular += specCo; + + finalColor = (texelColor*((colDiffuse + vec4(specular, 1.0))*vec4(lightDot, 1.0))); + + // Shadow calculations + vec4 fragPosLightSpace = lightVP * vec4(fragPosition, 1); + fragPosLightSpace.xyz /= fragPosLightSpace.w; // Perform the perspective division + fragPosLightSpace.xyz = (fragPosLightSpace.xyz + 1.0f) / 2.0f; // Transform from [-1, 1] range to [0, 1] range + vec2 sampleCoords = fragPosLightSpace.xy; + float curDepth = fragPosLightSpace.z; + // Slope-scale depth bias: depth biasing reduces "shadow acne" artifacts, where dark stripes appear all over the scene. + // The solution is adding a small bias to the depth + // In this case, the bias is proportional to the slope of the surface, relative to the light + float bias = max(0.0002 * (1.0 - dot(normal, l)), 0.00002) + 0.00001; + int shadowCounter = 0; + const int numSamples = 9; + // PCF (percentage-closer filtering) algorithm: + // Instead of testing if just one point is closer to the current point, + // we test the surrounding points as well. + // This blurs shadow edges, hiding aliasing artifacts. + vec2 texelSize = vec2(1.0f / float(shadowMapResolution)); + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float sampleDepth = texture(shadowMap, sampleCoords + texelSize * vec2(x, y)).r; + if (curDepth - bias > sampleDepth) + { + shadowCounter++; + } + } + } + finalColor = mix(finalColor, vec4(0, 0, 0, 1), float(shadowCounter) / float(numSamples)); + + // Add ambient lighting whether in shadow or not + finalColor += texelColor*(ambient/10.0)*colDiffuse; + + // Gamma correction + finalColor = pow(finalColor, vec4(1.0/2.2)); +} diff --git a/examples/shaders/resources/shaders/glsl330/shadowmap.vs b/examples/shaders/resources/shaders/glsl330/shadowmap.vs new file mode 100644 index 000000000..f8ec45f19 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/shadowmap.vs @@ -0,0 +1,32 @@ +#version 330 + +// Input vertex attributes +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; +in vec4 vertexColor; + +// Input uniform values +uniform mat4 mvp; +uniform mat4 matModel; +uniform mat4 matNormal; + +// Output vertex attributes (to fragment shader) +out vec3 fragPosition; +out vec2 fragTexCoord; +out vec4 fragColor; +out vec3 fragNormal; + +// NOTE: Add here your custom variables + +void main() +{ + // Send vertex attributes to fragment shader + fragPosition = vec3(matModel*vec4(vertexPosition, 1.0)); + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + fragNormal = normalize(vec3(matNormal*vec4(vertexNormal, 1.0))); + + // Calculate final vertex position + gl_Position = mvp*vec4(vertexPosition, 1.0); +} diff --git a/examples/shaders/shaders_shadowmap.c b/examples/shaders/shaders_shadowmap.c new file mode 100644 index 000000000..15ffe008d --- /dev/null +++ b/examples/shaders/shaders_shadowmap.c @@ -0,0 +1,251 @@ +/******************************************************************************************* +* +* raylib [shaders] example - Shadowmap +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example contributed by @TheManTheMythTheGameDev and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" + +#if defined(PLATFORM_DESKTOP) +#define GLSL_VERSION 330 +#else // PLATFORM_ANDROID, PLATFORM_WEB +#define GLSL_VERSION 120 +#endif + +#define SHADOWMAP_RESOLUTION 1024 + +RenderTexture2D LoadShadowmapRenderTexture(int width, int height); +void UnloadShadowmapRenderTexture(RenderTexture2D target); +void DrawScene(Model cube, Model robot); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + SetConfigFlags(FLAG_MSAA_4X_HINT); + // Shadows are a HUGE topic, and this example shows an extremely simple implementation of the shadowmapping algorithm, + // which is the industry standard for shadows. This algorithm can be extended in a ridiculous number of ways to improve + // realism and also adapt it for different scenes. This is pretty much the simplest possible implementation. + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - shadowmap"); + + Camera3D cam = (Camera3D){ 0 }; + cam.position = (Vector3){ 10.0f, 10.0f, 10.0f }; + cam.target = Vector3Zero(); + cam.projection = CAMERA_PERSPECTIVE; + cam.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + cam.fovy = 45.0f; + + Shader shadowShader = LoadShader(TextFormat("resources/shaders/glsl%i/shadowmap.vs", GLSL_VERSION), + TextFormat("resources/shaders/glsl%i/shadowmap.fs", GLSL_VERSION)); + shadowShader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(shadowShader, "viewPos"); + Vector3 lightDir = Vector3Normalize((Vector3){ 0.35f, -1.0f, -0.35f }); + Color lightColor = WHITE; + Vector4 lightColorNormalized = ColorNormalize(lightColor); + int lightDirLoc = GetShaderLocation(shadowShader, "lightDir"); + int lightColLoc = GetShaderLocation(shadowShader, "lightColor"); + SetShaderValue(shadowShader, lightDirLoc, &lightDir, SHADER_UNIFORM_VEC3); + SetShaderValue(shadowShader, lightColLoc, &lightColorNormalized, SHADER_UNIFORM_VEC4); + int ambientLoc = GetShaderLocation(shadowShader, "ambient"); + float ambient[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + SetShaderValue(shadowShader, ambientLoc, ambient, SHADER_UNIFORM_VEC4); + int lightVPLoc = GetShaderLocation(shadowShader, "lightVP"); + int shadowMapLoc = GetShaderLocation(shadowShader, "shadowMap"); + int shadowMapResolution = SHADOWMAP_RESOLUTION; + SetShaderValue(shadowShader, GetShaderLocation(shadowShader, "shadowMapResolution"), &shadowMapResolution, SHADER_UNIFORM_INT); + + Model cube = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + cube.materials[0].shader = shadowShader; + Model robot = LoadModel("resources/models/robot.glb"); + for (int i = 0; i < robot.materialCount; i++) + { + robot.materials[i].shader = shadowShader; + } + int animCount = 0; + ModelAnimation* robotAnimations = LoadModelAnimations("resources/models/robot.glb", &animCount); + + RenderTexture2D shadowMap = LoadShadowmapRenderTexture(SHADOWMAP_RESOLUTION, SHADOWMAP_RESOLUTION); + // For the shadowmapping algorithm, we will be rendering everything from the light's point of view + Camera3D lightCam = (Camera3D){ 0 }; + lightCam.position = Vector3Scale(lightDir, -15.0f); + lightCam.target = Vector3Zero(); + // Use an orthographic projection for directional lights + lightCam.projection = CAMERA_ORTHOGRAPHIC; + lightCam.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + lightCam.fovy = 20.0f; + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + int fc = 0; + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + float dt = GetFrameTime(); + + Vector3 cameraPos = cam.position; + SetShaderValue(shadowShader, shadowShader.locs[SHADER_LOC_VECTOR_VIEW], &cameraPos, SHADER_UNIFORM_VEC3); + UpdateCamera(&cam, CAMERA_ORBITAL); + + fc++; + fc %= (robotAnimations[0].frameCount); + UpdateModelAnimation(robot, robotAnimations[0], fc); + + const float cameraSpeed = 0.05f; + if (IsKeyDown(KEY_LEFT)) + { + if (lightDir.x < 0.6f) + lightDir.x += cameraSpeed * 60.0f * dt; + } + if (IsKeyDown(KEY_RIGHT)) + { + if (lightDir.x > -0.6f) + lightDir.x -= cameraSpeed * 60.0f * dt; + } + if (IsKeyDown(KEY_UP)) + { + if (lightDir.z < 0.6f) + lightDir.z += cameraSpeed * 60.0f * dt; + } + if (IsKeyDown(KEY_DOWN)) + { + if (lightDir.z > -0.6f) + lightDir.z -= cameraSpeed * 60.0f * dt; + } + lightDir = Vector3Normalize(lightDir); + lightCam.position = Vector3Scale(lightDir, -15.0f); + SetShaderValue(shadowShader, lightDirLoc, &lightDir, SHADER_UNIFORM_VEC3); + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + // First, render all objects into the shadowmap + // The idea is, we record all the objects' depths (as rendered from the light source's point of view) in a buffer + // Anything that is "visible" to the light is in light, anything that isn't is in shadow + // We can later use the depth buffer when rendering everything from the player's point of view + // to determine whether a given point is "visible" to the light + + // Record the light matrices for future use! + Matrix lightView; + Matrix lightProj; + BeginTextureMode(shadowMap); + ClearBackground(WHITE); + BeginMode3D(lightCam); + lightView = rlGetMatrixModelview(); + lightProj = rlGetMatrixProjection(); + DrawScene(cube, robot); + EndMode3D(); + EndTextureMode(); + Matrix lightViewProj = MatrixMultiply(lightView, lightProj); + + ClearBackground(RAYWHITE); + + SetShaderValueMatrix(shadowShader, lightVPLoc, lightViewProj); + + rlEnableShader(shadowShader.id); + int slot = 10; // Can be anything 0 to 15, but 0 will probably be taken up + rlActiveTextureSlot(10); + rlEnableTexture(shadowMap.depth.id); + rlSetUniform(shadowMapLoc, &slot, SHADER_UNIFORM_INT, 1); + + BeginMode3D(cam); + + // Draw the same exact things as we drew in the shadowmap! + DrawScene(cube, robot); + + EndMode3D(); + + DrawText("Shadows in raylib using the shadowmapping algorithm!", screenWidth - 320, screenHeight - 20, 10, GRAY); + DrawText("Use the arrow keys to rotate the light!", 10, 10, 30, RED); + + EndDrawing(); + + if (IsKeyPressed(KEY_F)) + { + TakeScreenshot("shaders_shadowmap.png"); + } + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + + UnloadShader(shadowShader); + UnloadModel(cube); + UnloadModel(robot); + UnloadModelAnimations(robotAnimations, animCount); + UnloadShadowmapRenderTexture(shadowMap); + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +RenderTexture2D LoadShadowmapRenderTexture(int width, int height) +{ + RenderTexture2D target = { 0 }; + + target.id = rlLoadFramebuffer(width, height); // Load an empty framebuffer + target.texture.width = width; + target.texture.height = height; + + if (target.id > 0) + { + rlEnableFramebuffer(target.id); + + // Create depth texture + // We don't need a color texture for the shadowmap + target.depth.id = rlLoadTextureDepth(width, height, false); + target.depth.width = width; + target.depth.height = height; + target.depth.format = 19; //DEPTH_COMPONENT_24BIT? + target.depth.mipmaps = 1; + + // Attach depth texture to FBO + rlFramebufferAttach(target.id, target.depth.id, RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_TEXTURE2D, 0); + + // Check if fbo is complete with attachments (valid) + if (rlFramebufferComplete(target.id)) TRACELOG(LOG_INFO, "FBO: [ID %i] Framebuffer object created successfully", target.id); + + rlDisableFramebuffer(); + } + else TRACELOG(LOG_WARNING, "FBO: Framebuffer object can not be created"); + + return target; +} + +// Unload shadowmap render texture from GPU memory (VRAM) +void UnloadShadowmapRenderTexture(RenderTexture2D target) +{ + if (target.id > 0) + { + // NOTE: Depth texture/renderbuffer is automatically + // queried and deleted before deleting framebuffer + rlUnloadFramebuffer(target.id); + } +} + +void DrawScene(Model cube, Model robot) +{ + DrawModelEx(cube, Vector3Zero(), (Vector3) { 0.0f, 1.0f, 0.0f }, 0.0f, (Vector3) { 10.0f, 1.0f, 10.0f }, BLUE); + DrawModelEx(cube, (Vector3) { 1.5f, 1.0f, -1.5f }, (Vector3) { 0.0f, 1.0f, 0.0f }, 0.0f, Vector3One(), WHITE); + DrawModelEx(robot, (Vector3) { 0.0f, 0.5f, 0.0f }, (Vector3) { 0.0f, 1.0f, 0.0f }, 0.0f, (Vector3) { 1.0f, 1.0f, 1.0f }, RED); +} diff --git a/examples/shaders/shaders_shadowmap.png b/examples/shaders/shaders_shadowmap.png new file mode 100644 index 000000000..2d180ee73 Binary files /dev/null and b/examples/shaders/shaders_shadowmap.png differ