diff --git a/examples/shaders/resources/shaders/glsl100/lights_bloom.fs b/examples/shaders/resources/shaders/glsl100/lights_bloom.fs new file mode 100644 index 000000000..a86cfe25c --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/lights_bloom.fs @@ -0,0 +1,44 @@ +#version 100 + +precision mediump float; + +// Input values from vertex shader +varying vec3 fragPosition; +varying vec3 fragNormal; +varying vec2 fragTexCoord; + +// Input uniform values +uniform sampler2D texture0; +uniform vec3 viewPos; +uniform vec3 lightPositions[8]; +uniform vec3 lightColors[8]; + +void main() +{ + vec4 texColor = texture2D(texture0, fragTexCoord); + vec3 norm = normalize(fragNormal); + vec3 viewDir = normalize(viewPos - fragPosition); + + vec3 ambient = 0.05*texColor.rgb; + vec3 totalDiffuse = vec3(0.0); + vec3 totalSpecular = vec3(0.0); + + for (int i = 0; i < 8; i++) + { + vec3 lightDir = normalize(lightPositions[i] - fragPosition); + float dist = length(lightPositions[i] - fragPosition); + float attenuation = 1.0/(1.0 + 0.8*dist + 0.4*dist*dist); + + // Diffuse shading + float diff = max(dot(norm, lightDir), 0.0); + totalDiffuse += diff*lightColors[i]*attenuation*1.2; + + // Blinn-Phong specular + vec3 halfwayDir = normalize(lightDir + viewDir); + float spec = pow(max(dot(norm, halfwayDir), 0.0), 32.0); + totalSpecular += spec*lightColors[i]*attenuation*0.8; + } + + vec3 result = ambient + (totalDiffuse*texColor.rgb) + totalSpecular; + gl_FragColor = vec4(result, texColor.a); +} diff --git a/examples/shaders/resources/shaders/glsl100/lights_bloom.vs b/examples/shaders/resources/shaders/glsl100/lights_bloom.vs new file mode 100644 index 000000000..8b088f9ab --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/lights_bloom.vs @@ -0,0 +1,23 @@ +#version 100 + +// Input vertex attributes +attribute vec3 vertexPosition; +attribute vec2 vertexTexCoord; +attribute vec3 vertexNormal; + +// Output values to fragment shader +varying vec3 fragPosition; +varying vec3 fragNormal; +varying vec2 fragTexCoord; + +// Input uniform values +uniform mat4 mvp; +uniform mat4 matModel; + +void main() +{ + fragPosition = vec3(matModel*vec4(vertexPosition, 1.0)); + fragNormal = normalize(vec3(matModel*vec4(vertexNormal, 0.0))); + fragTexCoord = vertexTexCoord; + gl_Position = mvp*vec4(vertexPosition, 1.0); +} diff --git a/examples/shaders/resources/shaders/glsl100/lights_bloom_post.fs b/examples/shaders/resources/shaders/glsl100/lights_bloom_post.fs new file mode 100644 index 000000000..82aad4702 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/lights_bloom_post.fs @@ -0,0 +1,33 @@ +#version 100 + +precision mediump float; + +varying vec2 fragTexCoord; + +uniform sampler2D texture0; + +void main() +{ + vec4 color = texture2D(texture0, fragTexCoord); + + // High-pass threshold blur: only pixels brighter than 0.75 in any channel contribute + vec4 bloom = vec4(0.0); + vec2 texel = vec2(1.0/1280.0, 1.0/720.0)*2.5; + + for (int x = -2; x <= 2; x++) + { + for (int y = -2; y <= 2; y++) + { + vec4 smp = texture2D(texture0, fragTexCoord + vec2(float(x), float(y))*texel); + float brightness = max(smp.r, max(smp.g, smp.b)); + if (brightness > 0.75) bloom += smp; + } + } + bloom /= 25.0; + + // Combine original with the bloom glow, then Reinhard tone-map back to [0, 1] + vec3 hdr = color.rgb + bloom.rgb*1.2; + vec3 ldr = hdr/(hdr + vec3(1.0)); + + gl_FragColor = vec4(ldr, color.a); +} diff --git a/examples/shaders/resources/shaders/glsl330/lights_bloom.fs b/examples/shaders/resources/shaders/glsl330/lights_bloom.fs new file mode 100644 index 000000000..ef3ed24cd --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/lights_bloom.fs @@ -0,0 +1,44 @@ +#version 330 + +// Input values from vertex shader +in vec3 fragPosition; +in vec3 fragNormal; +in vec2 fragTexCoord; + +out vec4 finalColor; + +// Input uniform values +uniform sampler2D texture0; +uniform vec3 viewPos; +uniform vec3 lightPositions[8]; +uniform vec3 lightColors[8]; + +void main() +{ + vec4 texColor = texture(texture0, fragTexCoord); + vec3 norm = normalize(fragNormal); + vec3 viewDir = normalize(viewPos - fragPosition); + + vec3 ambient = 0.05*texColor.rgb; + vec3 totalDiffuse = vec3(0.0); + vec3 totalSpecular = vec3(0.0); + + for (int i = 0; i < 8; i++) + { + vec3 lightDir = normalize(lightPositions[i] - fragPosition); + float dist = length(lightPositions[i] - fragPosition); + float attenuation = 1.0/(1.0 + 0.8*dist + 0.4*dist*dist); + + // Diffuse shading + float diff = max(dot(norm, lightDir), 0.0); + totalDiffuse += diff*lightColors[i]*attenuation*1.2; + + // Blinn-Phong specular + vec3 halfwayDir = normalize(lightDir + viewDir); + float spec = pow(max(dot(norm, halfwayDir), 0.0), 32.0); + totalSpecular += spec*lightColors[i]*attenuation*0.8; + } + + vec3 result = ambient + (totalDiffuse*texColor.rgb) + totalSpecular; + finalColor = vec4(result, texColor.a); +} diff --git a/examples/shaders/resources/shaders/glsl330/lights_bloom.vs b/examples/shaders/resources/shaders/glsl330/lights_bloom.vs new file mode 100644 index 000000000..f9e11c818 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/lights_bloom.vs @@ -0,0 +1,23 @@ +#version 330 + +// Input vertex attributes +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; + +// Output values to fragment shader +out vec3 fragPosition; +out vec3 fragNormal; +out vec2 fragTexCoord; + +// Input uniform values +uniform mat4 mvp; +uniform mat4 matModel; + +void main() +{ + fragPosition = vec3(matModel*vec4(vertexPosition, 1.0)); + fragNormal = normalize(vec3(matModel*vec4(vertexNormal, 0.0))); + fragTexCoord = vertexTexCoord; + gl_Position = mvp*vec4(vertexPosition, 1.0); +} diff --git a/examples/shaders/resources/shaders/glsl330/lights_bloom_post.fs b/examples/shaders/resources/shaders/glsl330/lights_bloom_post.fs new file mode 100644 index 000000000..8d7b63617 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/lights_bloom_post.fs @@ -0,0 +1,32 @@ +#version 330 + +in vec2 fragTexCoord; +out vec4 finalColor; + +uniform sampler2D texture0; + +void main() +{ + vec4 color = texture(texture0, fragTexCoord); + + // High-pass threshold blur: only pixels brighter than 0.75 in any channel contribute + vec4 bloom = vec4(0.0); + vec2 texel = vec2(1.0/1280.0, 1.0/720.0)*2.5; + + for (int x = -2; x <= 2; x++) + { + for (int y = -2; y <= 2; y++) + { + vec4 smp = texture(texture0, fragTexCoord + vec2(float(x), float(y))*texel); + float brightness = max(smp.r, max(smp.g, smp.b)); + if (brightness > 0.75) bloom += smp; + } + } + bloom /= 25.0; + + // Combine original with the bloom glow, then Reinhard tone-map back to [0, 1] + vec3 hdr = color.rgb + bloom.rgb*1.2; + vec3 ldr = hdr/(hdr + vec3(1.0)); + + finalColor = vec4(ldr, color.a); +} diff --git a/examples/shaders/shaders_lights_bloom.c b/examples/shaders/shaders_lights_bloom.c new file mode 100644 index 000000000..a5f85a27a --- /dev/null +++ b/examples/shaders/shaders_lights_bloom.c @@ -0,0 +1,198 @@ +/******************************************************************************************* +* +* raylib [shaders] example - forward multi-lighting with bloom +* +* Example demonstrates forward multi-point lighting (8 point lights, attenuation and +* Blinn-Phong specular) combined with a threshold-based bloom pass and Reinhard tone +* mapping, applied as a full-screen post-process pass +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example uses resources/shaders/glsl100 and resources/shaders/glsl330, shared with the +* rest of the shaders examples +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by PanicTitan (@PanicTitan) 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 +* +* Copyright (c) 2025 PanicTitan (@PanicTitan) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" + +#include // Required for: sinf(), cosf() + +#if defined(PLATFORM_DESKTOP) + #define GLSL_VERSION 330 +#else // PLATFORM_ANDROID, PLATFORM_WEB + #define GLSL_VERSION 100 +#endif + +//-------------------------------------------------------------------------------------- +// Global Definitions +//-------------------------------------------------------------------------------------- +#define MAX_LIGHTS 8 + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 1280; + const int screenHeight = 720; + + SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - forward multi-lighting bloom"); + + Camera3D camera = { 0 }; + camera.position = (Vector3){ 0.0f, 5.0f, 9.0f }; + camera.target = (Vector3){ 0.0f, 0.5f, 0.0f }; + camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + camera.fovy = 45.0f; + camera.projection = CAMERA_PERSPECTIVE; + + RenderTexture2D target = LoadRenderTexture(screenWidth, screenHeight); + + // Forward multi-light shader and bloom post-process shader, both loaded from the + // resources folder shared with the rest of the shaders examples. If the GLSL version + // guessed from the PLATFORM_DESKTOP build flag turns out to be wrong for whatever + // raylib was built against, linking fails and silently falls back to the default + // shader - so this retries once with the other version, and either way the result + // is checked explicitly and reported on screen rather than staying silently wrong + int glslVersion = GLSL_VERSION; + Shader lightShader = LoadShader(TextFormat("resources/shaders/glsl%i/lights_bloom.vs", glslVersion), + TextFormat("resources/shaders/glsl%i/lights_bloom.fs", glslVersion)); + Shader bloomShader = LoadShader(0, TextFormat("resources/shaders/glsl%i/lights_bloom_post.fs", glslVersion)); + + if ((lightShader.id == rlGetShaderIdDefault()) || (bloomShader.id == rlGetShaderIdDefault())) + { + glslVersion = (glslVersion == 330)? 100 : 330; + UnloadShader(lightShader); + UnloadShader(bloomShader); + lightShader = LoadShader(TextFormat("resources/shaders/glsl%i/lights_bloom.vs", glslVersion), + TextFormat("resources/shaders/glsl%i/lights_bloom.fs", glslVersion)); + bloomShader = LoadShader(0, TextFormat("resources/shaders/glsl%i/lights_bloom_post.fs", glslVersion)); + } + + bool shadersLoaded = (lightShader.id != rlGetShaderIdDefault()) && (bloomShader.id != rlGetShaderIdDefault()); + + Model cube = LoadModelFromMesh(GenMeshCube(2.0f, 2.0f, 2.0f)); + Model floor = LoadModelFromMesh(GenMeshPlane(14.0f, 14.0f, 1, 1)); + cube.materials[0].shader = lightShader; + floor.materials[0].shader = lightShader; + + int lightPosLoc = GetShaderLocation(lightShader, "lightPositions"); + int lightColLoc = GetShaderLocation(lightShader, "lightColors"); + int viewPosLoc = GetShaderLocation(lightShader, "viewPos"); + + Vector3 lightPositions[MAX_LIGHTS] = { 0 }; + Vector3 lightColors[MAX_LIGHTS] = { + { 1.0f, 0.2f, 0.2f }, // Red + { 0.2f, 1.0f, 0.3f }, // Green + { 0.2f, 0.5f, 1.0f }, // Blue + { 1.0f, 0.8f, 0.1f }, // Yellow + { 1.0f, 0.1f, 0.8f }, // Magenta + { 0.1f, 1.0f, 1.0f }, // Cyan + { 1.0f, 0.4f, 0.1f }, // Orange + { 0.7f, 0.2f, 1.0f }, // Purple + }; + + SetShaderValueV(lightShader, lightColLoc, lightColors, SHADER_UNIFORM_VEC3, MAX_LIGHTS); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&camera, CAMERA_ORBITAL); + + float time = (float)GetTime(); + + // Orbital path for each point light, its own radius/height offset by index + for (int i = 0; i < MAX_LIGHTS; i++) + { + float angle = (i/(float)MAX_LIGHTS)*2.0f*PI + time*0.6f; + float radius = 4.2f + sinf(time*1.2f + i)*0.4f; + float height = 1.0f + sinf(time*1.8f + i)*0.6f; + + lightPositions[i] = (Vector3){ sinf(angle)*radius, height, cosf(angle)*radius }; + } + + SetShaderValueV(lightShader, lightPosLoc, lightPositions, SHADER_UNIFORM_VEC3, MAX_LIGHTS); + SetShaderValue(lightShader, viewPosLoc, &camera.position, SHADER_UNIFORM_VEC3); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Render the lit scene to an offscreen texture + BeginTextureMode(target); + + ClearBackground((Color){ 12, 12, 18, 255 }); + + BeginMode3D(camera); + + DrawModel(cube, (Vector3){ 0.0f, 1.0f, 0.0f }, 1.0f, GRAY); + DrawModel(floor, (Vector3){ 0.0f, 0.0f, 0.0f }, 1.0f, DARKGRAY); + DrawGrid(10, 1.0f); + + // Glowing bulbs mark each light's position + for (int i = 0; i < MAX_LIGHTS; i++) + { + Color bulbColor = (Color){ + (unsigned char)(lightColors[i].x*255), + (unsigned char)(lightColors[i].y*255), + (unsigned char)(lightColors[i].z*255), + 255 }; + + DrawSphere(lightPositions[i], 0.15f, bulbColor); + } + + EndMode3D(); + + EndTextureMode(); + + // Present the offscreen texture through the bloom + tone mapping shader + BeginDrawing(); + + ClearBackground(BLACK); + + Rectangle srcRec = { 0, 0, (float)target.texture.width, (float)-target.texture.height }; + + BeginShaderMode(bloomShader); + // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) + DrawTextureRec(target.texture, srcRec, (Vector2){ 0, 0 }, WHITE); + EndShaderMode(); + + DrawText("BALANCED MULTI-LIGHT + REINHARD TONE MAPPED BLOOM", 20, 20, 20, GREEN); + DrawFPS(10, 10); + + if (!shadersLoaded) DrawText("WARNING: a custom shader failed to load, showing plain image", 20, screenHeight - 30, 10, RED); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(cube); + UnloadModel(floor); + UnloadShader(lightShader); + UnloadShader(bloomShader); + UnloadRenderTexture(target); + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} diff --git a/examples/shaders/shaders_lights_bloom.png b/examples/shaders/shaders_lights_bloom.png new file mode 100644 index 000000000..ba28b9ea5 Binary files /dev/null and b/examples/shaders/shaders_lights_bloom.png differ