diff --git a/examples/shaders/resources/shaders/glsl100/cel.fs b/examples/shaders/resources/shaders/glsl100/cel.fs new file mode 100644 index 000000000..dfc22d0c0 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/cel.fs @@ -0,0 +1,58 @@ +#version 100 + +precision mediump float; + +varying vec3 fragPosition; +varying vec2 fragTexCoord; +varying vec4 fragColor; +varying vec3 fragNormal; + +uniform sampler2D texture0; +uniform vec4 colDiffuse; +uniform vec3 viewPos; +uniform float numBands; + +struct Light { + int enabled; + int type; + vec3 position; + vec3 target; + vec4 color; +}; +uniform Light lights[4]; + +void main() +{ + vec4 texColor = texture2D(texture0, fragTexCoord); + vec3 baseColor = texColor.rgb * fragColor.rgb * colDiffuse.rgb; + vec3 norm = normalize(fragNormal); + + float lightAccum = 0.08; // ambient floor + + for (int i = 0; i < 4; i++) + { + if (lights[i].enabled == 1) // no continue in GLSL ES 1.0 + { + vec3 lightDir; + if (lights[i].type == 0) + { + // Directional: direction is from position toward target. + lightDir = normalize(lights[i].position - lights[i].target); + } + else + { + // Point: direction from surface to light. + lightDir = normalize(lights[i].position - fragPosition); + } + + float NdotL = max(dot(norm, lightDir), 0.0); + + // Quantize NdotL into numBands discrete steps. + float quantized = min(floor(NdotL * numBands), numBands - 1.0) / (numBands - 1.0); + lightAccum += quantized * lights[i].color.r; + } + } + + lightAccum = clamp(lightAccum, 0.0, 1.0); + gl_FragColor = vec4(baseColor * lightAccum, texColor.a * colDiffuse.a); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl100/cel.vs b/examples/shaders/resources/shaders/glsl100/cel.vs new file mode 100644 index 000000000..153842a9b --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/cel.vs @@ -0,0 +1,47 @@ +#version 100 + +attribute vec3 vertexPosition; +attribute vec2 vertexTexCoord; +attribute vec3 vertexNormal; +attribute vec4 vertexColor; + +uniform mat4 mvp; +uniform mat4 matModel; + +varying vec3 fragPosition; +varying vec2 fragTexCoord; +varying vec4 fragColor; +varying vec3 fragNormal; + +mat3 inverse(mat3 m) +{ + float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; + float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2]; + float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2]; + float b01 = a22*a11 - a12*a21; + float b11 = -a22*a10 + a12*a20; + float b21 = a21*a10 - a11*a20; + float det = a00*b01 + a01*b11 + a02*b21; + return mat3(b01, (-a22*a01 + a02*a21), ( a12*a01 - a02*a11), + b11, ( a22*a00 - a02*a20), (-a12*a00 + a02*a10), + b21, (-a21*a00 + a01*a20), ( a11*a00 - a01*a10)) / det; +} + +mat3 transpose(mat3 m) +{ + return mat3(m[0][0], m[1][0], m[2][0], + m[0][1], m[1][1], m[2][1], + m[0][2], m[1][2], m[2][2]); +} + +void main() +{ + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + + mat3 normalMatrix = transpose(inverse(mat3(matModel))); + fragNormal = normalize(normalMatrix * vertexNormal); + + gl_Position = mvp * vec4(vertexPosition, 1.0); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl100/outline_hull.fs b/examples/shaders/resources/shaders/glsl100/outline_hull.fs new file mode 100644 index 000000000..4f82cfc87 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/outline_hull.fs @@ -0,0 +1,8 @@ +#version 100 + +precision mediump float; + +void main() +{ + gl_FragColor = vec4(0.05, 0.05, 0.05, 1.0); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl100/outline_hull.vs b/examples/shaders/resources/shaders/glsl100/outline_hull.vs new file mode 100644 index 000000000..af7be8831 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl100/outline_hull.vs @@ -0,0 +1,15 @@ +#version 100 + +attribute vec3 vertexPosition; +attribute vec3 vertexNormal; +attribute vec2 vertexTexCoord; +attribute vec4 vertexColor; + +uniform mat4 mvp; +uniform float outlineThickness; + +void main() +{ + vec3 extruded = vertexPosition + vertexNormal * outlineThickness; + gl_Position = mvp * vec4(extruded, 1.0); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl120/cel.fs b/examples/shaders/resources/shaders/glsl120/cel.fs new file mode 100644 index 000000000..88321e60f --- /dev/null +++ b/examples/shaders/resources/shaders/glsl120/cel.fs @@ -0,0 +1,56 @@ +#version 120 + +varying vec3 fragPosition; +varying vec2 fragTexCoord; +varying vec4 fragColor; +varying vec3 fragNormal; + +uniform sampler2D texture0; +uniform vec4 colDiffuse; +uniform vec3 viewPos; +uniform float numBands; + +struct Light { + int enabled; + int type; + vec3 position; + vec3 target; + vec4 color; +}; +uniform Light lights[4]; + +void main() +{ + vec4 texColor = texture2D(texture0, fragTexCoord); + vec3 baseColor = texColor.rgb * fragColor.rgb * colDiffuse.rgb; + vec3 norm = normalize(fragNormal); + + float lightAccum = 0.08; // ambient floor + + for (int i = 0; i < 4; i++) + { + if (lights[i].enabled == 1) + { + vec3 lightDir; + if (lights[i].type == 0) + { + // Directional: direction is from position toward target. + lightDir = normalize(lights[i].position - lights[i].target); + } + else + { + // Point: direction from surface to light. + lightDir = normalize(lights[i].position - fragPosition); + } + + float NdotL = max(dot(norm, lightDir), 0.0); + + // Quantize NdotL into numBands discrete steps. + float quantized = min(floor(NdotL * numBands), numBands - 1.0) / (numBands - 1.0); + lightAccum += quantized * lights[i].color.r; + } + } + + lightAccum = clamp(lightAccum, 0.0, 1.0); + gl_FragColor = vec4(baseColor * lightAccum, texColor.a * colDiffuse.a); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl120/cel.vs b/examples/shaders/resources/shaders/glsl120/cel.vs new file mode 100644 index 000000000..3c736aec7 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl120/cel.vs @@ -0,0 +1,48 @@ +#version 120 + +attribute vec3 vertexPosition; +attribute vec2 vertexTexCoord; +attribute vec3 vertexNormal; +attribute vec4 vertexColor; + +uniform mat4 mvp; +uniform mat4 matModel; + +varying vec3 fragPosition; +varying vec2 fragTexCoord; +varying vec4 fragColor; +varying vec3 fragNormal; + +// inverse() and transpose() are not built-in until GLSL 1.40 +mat3 inverse(mat3 m) +{ + float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; + float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2]; + float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2]; + float b01 = a22*a11 - a12*a21; + float b11 = -a22*a10 + a12*a20; + float b21 = a21*a10 - a11*a20; + float det = a00*b01 + a01*b11 + a02*b21; + return mat3(b01, (-a22*a01 + a02*a21), ( a12*a01 - a02*a11), + b11, ( a22*a00 - a02*a20), (-a12*a00 + a02*a10), + b21, (-a21*a00 + a01*a20), ( a11*a00 - a01*a10)) / det; +} + +mat3 transpose(mat3 m) +{ + return mat3(m[0][0], m[1][0], m[2][0], + m[0][1], m[1][1], m[2][1], + m[0][2], m[1][2], m[2][2]); +} + +void main() +{ + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + + mat3 normalMatrix = transpose(inverse(mat3(matModel))); + fragNormal = normalize(normalMatrix * vertexNormal); + + gl_Position = mvp * vec4(vertexPosition, 1.0); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl120/outline_hull.fs b/examples/shaders/resources/shaders/glsl120/outline_hull.fs new file mode 100644 index 000000000..b6e4fc828 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl120/outline_hull.fs @@ -0,0 +1,6 @@ +#version 120 + +void main() +{ + gl_FragColor = vec4(0.05, 0.05, 0.05, 1.0); +} diff --git a/examples/shaders/resources/shaders/glsl120/outline_hull.vs b/examples/shaders/resources/shaders/glsl120/outline_hull.vs new file mode 100644 index 000000000..7ad125be1 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl120/outline_hull.vs @@ -0,0 +1,15 @@ +#version 120 + +attribute vec3 vertexPosition; +attribute vec3 vertexNormal; +attribute vec2 vertexTexCoord; +attribute vec4 vertexColor; + +uniform mat4 mvp; +uniform float outlineThickness; + +void main() +{ + vec3 extruded = vertexPosition + vertexNormal * outlineThickness; + gl_Position = mvp * vec4(extruded, 1.0); +} \ No newline at end of file diff --git a/examples/shaders/resources/shaders/glsl330/cel.fs b/examples/shaders/resources/shaders/glsl330/cel.fs new file mode 100644 index 000000000..78a2097c2 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/cel.fs @@ -0,0 +1,60 @@ +#version 330 + +in vec3 fragPosition; +in vec2 fragTexCoord; +in vec4 fragColor; +in vec3 fragNormal; + +// Raylib standard uniforms +uniform sampler2D texture0; +uniform vec4 colDiffuse; + +// View position for future specular / fresnel use. +uniform vec3 viewPos; + +// Number of discrete toon bands (2 = hard binary, 10 = default, 20 = near-smooth). +uniform float numBands; + +// rlights.h compatible light block. +struct Light { + int enabled; + int type; // 0 = directional, 1 = point + vec3 position; + vec3 target; + vec4 color; + float attenuation; +}; +uniform Light lights[4]; + +out vec4 finalColor; + +void main() { + vec4 texColor = texture(texture0, fragTexCoord); + vec3 baseColor = texColor.rgb * fragColor.rgb * colDiffuse.rgb; + vec3 norm = normalize(fragNormal); + + float lightAccum = 0.08; // ambient floor + + for (int i = 0; i < 4; i++) { + if (lights[i].enabled == 0) continue; + + vec3 lightDir; + if (lights[i].type == 0) { + // Directional: direction is from position toward target. + lightDir = normalize(lights[i].position - lights[i].target); + } else { + // Point: direction from surface to light. + lightDir = normalize(lights[i].position - fragPosition); + } + + float NdotL = max(dot(norm, lightDir), 0.0); + + // Quantize NdotL into numBands discrete steps. + // min() guards against NdotL == 1.0 producing an out-of-range index. + float quantized = min(floor(NdotL * numBands), numBands - 1.0) / (numBands - 1.0); + lightAccum += quantized * lights[i].color.r; + } + + lightAccum = clamp(lightAccum, 0.0, 1.0); + finalColor = vec4(baseColor * lightAccum, texColor.a * colDiffuse.a); +} diff --git a/examples/shaders/resources/shaders/glsl330/cel.vs b/examples/shaders/resources/shaders/glsl330/cel.vs new file mode 100644 index 000000000..37a8d200d --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/cel.vs @@ -0,0 +1,25 @@ +#version 330 + +// Raylib standard attributes +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; +in vec4 vertexColor; + +// Raylib standard uniforms +uniform mat4 mvp; +uniform mat4 matModel; +uniform mat4 matNormal; + +out vec3 fragPosition; +out vec2 fragTexCoord; +out vec4 fragColor; +out vec3 fragNormal; + +void main() { + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 0.0))); + gl_Position = mvp * vec4(vertexPosition, 1.0); +} diff --git a/examples/shaders/resources/shaders/glsl330/outline_hull.fs b/examples/shaders/resources/shaders/glsl330/outline_hull.fs new file mode 100644 index 000000000..c8a33de08 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/outline_hull.fs @@ -0,0 +1,7 @@ +#version 330 + +out vec4 finalColor; + +void main() { + finalColor = vec4(0.05, 0.05, 0.05, 1.0); +} diff --git a/examples/shaders/resources/shaders/glsl330/outline_hull.vs b/examples/shaders/resources/shaders/glsl330/outline_hull.vs new file mode 100644 index 000000000..2e350201e --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/outline_hull.vs @@ -0,0 +1,15 @@ +#version 330 + +in vec3 vertexPosition; +in vec3 vertexNormal; +in vec2 vertexTexCoord; +in vec4 vertexColor; + +uniform mat4 mvp; +uniform float outlineThickness; + +void main() { + // Extrude vertex along its normal to create the hull. + vec3 extruded = vertexPosition + vertexNormal * outlineThickness; + gl_Position = mvp * vec4(extruded, 1.0); +} diff --git a/examples/shaders/shaders_cel_shading.c b/examples/shaders/shaders_cel_shading.c new file mode 100644 index 000000000..cab86a26f --- /dev/null +++ b/examples/shaders/shaders_cel_shading.c @@ -0,0 +1,202 @@ +/******************************************************************************************* +* +* raylib [shaders] example - cel shading +* +* Example complexity rating: [★★★☆] 3/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) +* +* Example contributed by Gleb A (@ggrizzly) +* +* 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) 2015-2026 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" +#include +#include + +#define RLIGHTS_IMPLEMENTATION +#include "rlights.h" + +#if defined(PLATFORM_DESKTOP) + #define GLSL_VERSION 330 +#else // PLATFORM_ANDROID, PLATFORM_WEB + #define GLSL_VERSION 100 +#endif + +//------------------------------------------------------------------------------------ +// Model table: path, optional diffuse texture path (NULL = embedded), draw scale +//------------------------------------------------------------------------------------ +typedef struct { + const char *modelPath; + const char *texturePath; // NULL for GLB files with embedded textures + float scale; + float outlineThickness; +} ModelInfo; + +static const ModelInfo MODEL = { "resources/models/old_car_new.glb", NULL, 0.75f, 0.005f }; + + +//------------------------------------------------------------------------------------ +// Load model and its diffuse texture (if any). Does NOT assign a shader. +//------------------------------------------------------------------------------------ +static Model celLoadModel() +{ + Model model = LoadModel(MODEL.modelPath); + + if (MODEL.texturePath != NULL) + { + Texture2D tex = LoadTexture(MODEL.texturePath); + model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = tex; + } + + return model; +} + +static void ApplyShaderToModel(Model model, Shader shader) +{ + model.materials[0].shader = shader; +} + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + SetConfigFlags(FLAG_MSAA_4X_HINT); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - cel shading"); + + Camera camera = { 0 }; + camera.position = (Vector3){ 9.0f, 6.0f, 9.0f }; + camera.target = (Vector3){ 0.0f, 1.0f, 0.0f }; + camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + camera.fovy = 45.0f; + camera.projection = CAMERA_PERSPECTIVE; + + // Load cel shader + Shader celShader = LoadShader(TextFormat("resources/shaders/glsl%i/cel.vs", GLSL_VERSION), + TextFormat("resources/shaders/glsl%i/cel.fs", GLSL_VERSION)); + celShader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(celShader, "viewPos"); + + // numBands: controls toon quantization steps (2 = hard binary, 20 = near-smooth) + float numBands = 10.0f; + int numBandsLoc = GetShaderLocation(celShader, "numBands"); + SetShaderValue(celShader, numBandsLoc, &numBands, SHADER_UNIFORM_FLOAT); + + // Inverted-hull outline shader: draws back faces extruded along normals + Shader outlineShader = LoadShader( + TextFormat("resources/shaders/glsl%i/outline_hull.vs", GLSL_VERSION), + TextFormat("resources/shaders/glsl%i/outline_hull.fs", GLSL_VERSION)); + int outlineThicknessLoc = GetShaderLocation(outlineShader, "outlineThickness"); + + // Single directional white light, angled so toon bands are visible on the model sides. + // Spins opposite to CAMERA_ORBITAL (0.5 rad/s) so lighting changes as you watch. + Light lights[MAX_LIGHTS] = { 0 }; + lights[0] = CreateLight(LIGHT_DIRECTIONAL, (Vector3){ 50.0f, 50.0f, 50.0f }, Vector3Zero(), WHITE, celShader); + + + bool celEnabled = true; + bool outlineEnabled = true; + + Model model = celLoadModel(); + Shader defaultShader = model.materials[0].shader; + ApplyShaderToModel(model, celShader); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&camera, CAMERA_ORBITAL); + + float cameraPos[3] = { camera.position.x, camera.position.y, camera.position.z }; + SetShaderValue(celShader, celShader.locs[SHADER_LOC_VECTOR_VIEW], cameraPos, SHADER_UNIFORM_VEC3); + + // [Z] Toggle cel shading on/off + if (IsKeyPressed(KEY_Z)) + { + celEnabled = !celEnabled; + ApplyShaderToModel(model, celEnabled ? celShader : defaultShader); + } + + // [C] Toggle outline on/off + if (IsKeyPressed(KEY_C)) outlineEnabled = !outlineEnabled; + + // [Q/E] Decrease/increase toon band count (press or hold to repeat) + if (IsKeyPressed(KEY_E) || IsKeyPressedRepeat(KEY_E)) numBands = Clamp(numBands + 1.0f, 2.0f, 20.0f); + if (IsKeyPressed(KEY_Q) || IsKeyPressedRepeat(KEY_Q)) numBands = Clamp(numBands - 1.0f, 2.0f, 20.0f); + SetShaderValue(celShader, numBandsLoc, &numBands, SHADER_UNIFORM_FLOAT); + + // Spin light opposite to CAMERA_ORBITAL (0.5 rad/s), angled 45 degrees off vertical + float t = (float)GetTime(); + lights[0].position = (Vector3){ + sinf(-t * 0.3f) * 5.0f, + 5.0f, + cosf(-t * 0.3f) * 5.0f + }; + + for (int i = 0; i < MAX_LIGHTS; i++) UpdateLightValues(celShader, lights[i]); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + BeginMode3D(camera); + + if (outlineEnabled) + { + // Outline pass: cull front faces, draw extruded back faces as silhouette + float thickness = MODEL.outlineThickness; + SetShaderValue(outlineShader, outlineThicknessLoc, &thickness, SHADER_UNIFORM_FLOAT); + rlSetCullFace(RL_CULL_FACE_FRONT); + ApplyShaderToModel(model, outlineShader); + DrawModel(model, Vector3Zero(), MODEL.scale, WHITE); + ApplyShaderToModel(model, celEnabled ? celShader : defaultShader); + rlSetCullFace(RL_CULL_FACE_BACK); + } + + DrawModel(model, Vector3Zero(), MODEL.scale, WHITE); + DrawSphereEx(lights[0].position, 0.2f, 50, 50, YELLOW); // Light position indicator + DrawGrid(10, 10.0f); + + EndMode3D(); + + DrawFPS(10, 10); + DrawText(TextFormat("Cel: %s [Z]", celEnabled ? "ON" : "OFF"), 10, 65, 20, celEnabled ? DARKGREEN : DARKGRAY); + DrawText(TextFormat("Outline: %s [C]", outlineEnabled ? "ON" : "OFF"), 10, 90, 20, outlineEnabled ? DARKGREEN : DARKGRAY); + DrawText(TextFormat("Bands: %.0f [Q/E]", numBands), 10, 115, 20, DARKGRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(model); + UnloadShader(celShader); + UnloadShader(outlineShader); + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; +} diff --git a/examples/shaders/shaders_cel_shading.png b/examples/shaders/shaders_cel_shading.png new file mode 100644 index 000000000..51b98225e Binary files /dev/null and b/examples/shaders/shaders_cel_shading.png differ