← ziglang.inraylib-zigGitHub

procedural decals

★★★★by PanicTitan (@PanicTitan)

There is no browser build of this example yet.

Is it the same as C?

//! raylib [models] example - procedural decals
//!
//! Example demonstrates projecting decals onto a 3D mesh surface by transforming the
//! mesh's own triangles into the decal's local space and clipping them against a box,
//! using only procedurally generated geometry and textures (no external resource files)
//!
//! NOTE: This is the same clip-space decal projection idea used by models_decals.c
//! (in turn based on three.js' DecalGeometry), applied here to a generated mesh and
//! generated textures instead of a loaded model, so the example is fully self-contained
//!
//! Zig-API translation of examples/models/models_procedural_decals.c (raylib 6.2).
//! Example contributed by PanicTitan (@PanicTitan) and reviewed by Ramon Santamaria (@raysan5)
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2025 PanicTitan (@PanicTitan)

const std = @import("std");
const rl = @import("raylib");
const gui = @import("raygui");
const math = rl.math;

const max_decals = 256;

pub fn main() !void {
    var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
    defer _ = debug_allocator.deinit();
    const gpa = debug_allocator.allocator();

    // Initialization
    const screen_width = 800;
    const screen_height = 450;

    rl.setConfigFlags(.{ .msaa_4x_hint = true });
    rl.initWindow(screen_width, screen_height, "raylib [models] example - procedural decals");
    defer rl.closeWindow(); // Close window and OpenGL context

    var camera: rl.Camera3D = .{
        .position = .{ .x = 0.0, .y = 2.5, .z = 5.0 },
        .target = .{ .x = 0.0, .y = 0.0, .z = 0.0 },
        .up = .{ .x = 0.0, .y = 1.0, .z = 0.0 },
        .fovy = 45.0,
        .projection = .perspective,
    };

    // Target model: a procedurally generated knot with a checkerboard skin
    const model = try rl.loadModelFromMesh(rl.genMeshTorus(0.8, 1.8, 32, 64));
    defer rl.unloadModel(model);

    const checker_image = rl.genImageChecked(512, 512, 32, 32, .light_gray, .gray);
    const model_texture = try rl.loadTextureFromImage(checker_image);
    defer rl.unloadTexture(model_texture);
    rl.unloadImage(checker_image);
    rl.setTextureFilter(model_texture, .bilinear);
    model.materials.?[0].map(.diffuse).texture = model_texture;

    const model_bbox = rl.getMeshBoundingBox(model.meshes.?[0]);
    camera.target = math.vector3Lerp(model_bbox.min, model_bbox.max, 0.5);

    const model_size = @min(
        @min(@abs(model_bbox.max.x - model_bbox.min.x), @abs(model_bbox.max.y - model_bbox.min.y)),
        @abs(model_bbox.max.z - model_bbox.min.z),
    );

    const decal_size = model_size * 0.35;
    const decal_offset = 0.01;

    // Translucent cube previewing where the next decal will land
    var placement_cube = try rl.loadModelFromMesh(rl.genMeshCube(decal_size, decal_size, decal_size));
    defer rl.unloadModel(placement_cube);
    placement_cube.materials.?[0].map(.diffuse).color = .lime;

    // Decal texture: a procedurally drawn target/bullseye, no image file needed
    var decal_image = rl.genImageColor(128, 128, .blank);
    rl.imageDrawCircle(&decal_image, 64, 64, 60, .red);
    rl.imageDrawCircle(&decal_image, 64, 64, 45, .white);
    rl.imageDrawCircle(&decal_image, 64, 64, 30, .red);
    rl.imageDrawCircle(&decal_image, 64, 64, 15, .yellow);

    const decal_texture = try rl.loadTextureFromImage(decal_image);
    rl.unloadImage(decal_image);
    rl.setTextureFilter(decal_texture, .bilinear);

    const decal_material = rl.loadMaterialDefault();
    defer rl.unloadMaterial(decal_material); // Also unloads its diffuse map texture (decal_texture)
    decal_material.map(.diffuse).texture = decal_texture;
    decal_material.map(.diffuse).color = .ray_white;

    var show_model = true;
    var decals: [max_decals]rl.Model = undefined;
    var decal_count: usize = 0;
    defer for (decals[0..decal_count]) |decal| rl.unloadModel(decal);

    const zero = math.vector3Zero();

    rl.setTargetFPS(60);

    // Main game loop
    while (!rl.windowShouldClose()) { // Detect window close button or ESC key
        // Update
        if (rl.isMouseButtonDown(.right)) rl.updateCamera(&camera, .third_person);

        // Cast a ray from the mouse and keep the closest point it hits on the model
        const ray = rl.getScreenToWorldRay(rl.getMousePosition(), camera);
        var collision = std.mem.zeroes(rl.RayCollision);

        if (rl.getRayCollisionBox(ray, model_bbox).hit and decal_count < max_decals) {
            for (model.meshSlice()) |mesh| {
                const mesh_hit = rl.getRayCollisionMesh(ray, mesh, model.transform);
                if (mesh_hit.hit and (!collision.hit or mesh_hit.distance < collision.distance)) collision = mesh_hit;
            }
        }

        // Project a new decal at the hit point, facing along the surface normal
        if (collision.hit and rl.isMouseButtonPressed(.left) and decal_count < max_decals) {
            const look_target = math.vector3Add(collision.point, collision.normal);
            var splat = math.matrixLookAt(collision.point, look_target, .{ .x = 0.0, .y = 1.0, .z = 0.0 });
            splat = math.matrixMultiply(splat, math.matrixRotateZ(rl.deg2rad * @as(f32, @floatFromInt(rl.getRandomValue(-180, 180)))));

            if (try genMeshDecal(gpa, model, splat, decal_size, decal_offset)) |decal_mesh| {
                const decal = try rl.loadModelFromMesh(decal_mesh);
                decal.materials.?[0].map(.diffuse).* = decal_material.map(.diffuse).*;
                decals[decal_count] = decal;
                decal_count += 1;
            }
        }

        // Draw
        rl.beginDrawing();
        defer rl.endDrawing();

        rl.clearBackground(.ray_white);

        {
            rl.beginMode3D(camera);
            defer rl.endMode3D();

            if (show_model) rl.drawModel(model, zero, 1.0, .white);

            for (decals[0..decal_count]) |decal| rl.drawModel(decal, zero, 1.0, .white);

            // Preview cube at the surface point currently under the mouse
            if (collision.hit) {
                const look_target = math.vector3Add(collision.point, collision.normal);
                const splat = math.matrixLookAt(collision.point, look_target, .{ .x = 0.0, .y = 1.0, .z = 0.0 });
                placement_cube.transform = math.matrixInvert(splat);
                rl.drawModel(placement_cube, zero, 1.0, rl.fade(.white, 0.5));
            }

            rl.drawGrid(10, 1.0);
        }

        // Vertex/triangle counts: base model vs total once decal geometry is added
        var base_vertices: i32 = 0;
        var base_triangles: i32 = 0;
        for (model.meshSlice()) |mesh| {
            base_vertices += mesh.vertexCount;
            base_triangles += mesh.triangleCount;
        }

        var total_vertices = base_vertices;
        var total_triangles = base_triangles;
        for (decals[0..decal_count]) |decal| {
            total_vertices += decal.meshes.?[0].vertexCount;
            total_triangles += decal.meshes.?[0].triangleCount;
        }

        const stat_x = screen_width - 280;
        rl.drawText("Vertices", stat_x + 90, 10, 10, .lime);
        rl.drawText("Triangles", stat_x + 180, 10, 10, .lime);
        rl.drawText("Base Model", stat_x, 25, 10, .lime);
        rl.drawText(rl.textFormat("{d}", .{base_vertices}), stat_x + 90, 25, 10, .lime);
        rl.drawText(rl.textFormat("{d}", .{base_triangles}), stat_x + 180, 25, 10, .lime);
        rl.drawText("TOTAL", stat_x, 40, 10, .lime);
        rl.drawText(rl.textFormat("{d}", .{total_vertices}), stat_x + 90, 40, 10, .lime);
        rl.drawText(rl.textFormat("{d}", .{total_triangles}), stat_x + 180, 40, 10, .lime);

        if (gui.button(.{ .x = 10, .y = screen_height - 80.0, .width = 100, .height = 40 }, if (show_model) "Hide Model" else "Show Model") != 0) {
            show_model = !show_model;
        }

        if (gui.button(.{ .x = 120, .y = screen_height - 80.0, .width = 100, .height = 40 }, "Clear Decals") != 0) {
            for (decals[0..decal_count]) |decal| rl.unloadModel(decal);
            decal_count = 0;
        }

        rl.drawText("Left click: place decal   |   Right drag: orbit camera", 10, screen_height - 25, 10, .dark_gray);
        rl.drawFPS(10, 10);
    }
}

/// A triangle soup: vertices in groups of three.
const TriangleList = std.ArrayList(rl.Vector3);

/// Clips the segment [v0, v1] against the plane p.x = s, returning the intersection point
fn clipSegment(v0: rl.Vector3, v1: rl.Vector3, p: rl.Vector3, s: f32) rl.Vector3 {
    const d0 = math.vector3DotProduct(v0, p) - s;
    const d1 = math.vector3DotProduct(v1, p) - s;
    const t = d0 / (d0 - d1);

    return math.vector3Lerp(v0, v1, t);
}

/// Builds a decal mesh: the model's triangles are transformed into the decal's local space
/// (so the decal sits at the origin, facing +Z) and clipped against a decal_size-sided box,
/// following the same clip-space projection idea used by engines' decal systems (and by
/// three.js' DecalGeometry, which the technique is commonly traced back to). What's left
/// after clipping becomes the decal's own small mesh, UV-mapped from its local coordinates.
/// Returns null when nothing of the model lies inside the decal box.
fn genMeshDecal(gpa: std.mem.Allocator, model: rl.Model, projection: rl.Matrix, decal_size: f32, decal_offset: f32) !?rl.Mesh {
    const inv_proj = math.matrixInvert(projection);

    // Two triangle lists: each clipping pass reads from one and writes to the other
    var lists: [2]TriangleList = .{ .empty, .empty };
    defer for (&lists) |*list| list.deinit(gpa);
    var list_index: usize = 0;

    // Gather triangles that land anywhere near the decal box; this is a loose, cheap
    // pre-filter meant to skip most of the model, the precise clip happens below
    for (model.meshSlice()) |mesh| {
        const vertices = mesh.vertices.?;

        for (0..@intCast(mesh.triangleCount)) |tri| {
            var triangle: [3]rl.Vector3 = undefined;

            for (&triangle, 0..) |*vertex, v| {
                const index: usize = if (mesh.indices) |indices| indices[3 * tri + v] else 3 * tri + v;
                vertex.* = .{ .x = vertices[3 * index + 0], .y = vertices[3 * index + 1], .z = vertices[3 * index + 2] };
            }

            var inside_count: usize = 0;
            for (&triangle) |*vertex| {
                const v = math.vector3Transform(vertex.*, projection);
                if (@abs(v.x) < decal_size or @abs(v.y) <= decal_size or @abs(v.z) <= decal_size) inside_count += 1;
                vertex.* = v;
            }

            if (inside_count > 0) try lists[list_index].appendSlice(gpa, &triangle);
        }
    }

    // Clip the surviving triangles against each of the decal box's 6 faces in turn
    const planes = [6]rl.Vector3{
        .{ .x = 1, .y = 0, .z = 0 }, .{ .x = -1, .y = 0, .z = 0 },
        .{ .x = 0, .y = 1, .z = 0 }, .{ .x = 0, .y = -1, .z = 0 },
        .{ .x = 0, .y = 0, .z = 1 }, .{ .x = 0, .y = 0, .z = -1 },
    };

    const s = 0.5 * decal_size;

    for (planes) |plane| {
        list_index = 1 - list_index;

        const in = lists[1 - list_index].items;
        const out = &lists[list_index];
        out.clearRetainingCapacity();

        for (0..in.len / 3) |t| {
            const v = in[3 * t ..][0..3].*;

            var outside: [3]bool = undefined;
            for (v, &outside) |vertex, *is_out| is_out.* = math.vector3DotProduct(vertex, plane) - s > 0;
            const total = @as(u8, @intFromBool(outside[0])) + @intFromBool(outside[1]) + @intFromBool(outside[2]);

            switch (total) {
                // Whole triangle is inside this face, keep it as-is
                0 => try out.appendSlice(gpa, &v),
                // One corner is outside; clip it off, turning the triangle into a quad
                1 => if (outside[0]) {
                    const n1 = v[1];
                    const n2 = v[2];
                    const n3 = clipSegment(v[0], n1, plane, s);
                    const n4 = clipSegment(v[0], n2, plane, s);
                    try out.appendSlice(gpa, &.{ n1, n2, n3, n4, n3, n2 });
                } else if (outside[1]) {
                    const n1 = v[0];
                    const n2 = v[2];
                    const n3 = clipSegment(v[1], n1, plane, s);
                    const n4 = clipSegment(v[1], n2, plane, s);
                    try out.appendSlice(gpa, &.{ n3, n2, n1, n2, n3, n4 });
                } else {
                    const n1 = v[0];
                    const n2 = v[1];
                    const n3 = clipSegment(v[2], n1, plane, s);
                    const n4 = clipSegment(v[2], n2, plane, s);
                    try out.appendSlice(gpa, &.{ n1, n2, n3, n4, n3, n2 });
                },
                // Two corners are outside; only the small corner near the surviving vertex remains
                2 => {
                    const k = std.mem.indexOfScalar(bool, &outside, false).?;
                    const n1 = v[k];
                    const n2 = clipSegment(n1, v[(k + 1) % 3], plane, s);
                    const n3 = clipSegment(n1, v[(k + 2) % 3], plane, s);
                    try out.appendSlice(gpa, &.{ n1, n2, n3 });
                },
                // All 3 corners are outside this face, the triangle is fully clipped away
                else => {},
            }
        }
    }

    const decal = lists[list_index].items;
    if (decal.len == 0) return null;

    // Converts the triangle soup into a standard uploaded raylib Mesh (its buffers are owned and freed by raylib)
    const vertices = try rl.mem_allocator.alloc(f32, decal.len * 3);
    errdefer rl.mem_allocator.free(vertices);
    const texcoords = try rl.mem_allocator.alloc(f32, decal.len * 2);
    errdefer rl.mem_allocator.free(texcoords);

    for (decal, 0..) |vertex, i| {
        // Clipped coordinates run roughly (-decal_size/2 .. decal_size/2); remap to (0..1)
        texcoords[2 * i + 0] = vertex.x / decal_size + 0.5;
        texcoords[2 * i + 1] = vertex.y / decal_size + 0.5;

        // Nudge slightly along the normal so the decal doesn't z-fight with the surface
        var nudged = vertex;
        nudged.z -= decal_offset;
        const world = math.vector3Transform(nudged, inv_proj);
        vertices[3 * i ..][0..3].* = .{ world.x, world.y, world.z };
    }

    var mesh = std.mem.zeroes(rl.Mesh);
    mesh.vertexCount = @intCast(decal.len);
    mesh.triangleCount = @intCast(decal.len / 3);
    mesh.vertices = vertices.ptr;
    mesh.texcoords = texcoords.ptr;

    rl.uploadMesh(&mesh, false);

    return mesh;
}
/*******************************************************************************************
*
*   raylib [models] example - procedural decals
*
*   Example demonstrates projecting decals onto a 3D mesh surface by transforming the
*   mesh's own triangles into the decal's local space and clipping them against a box,
*   using only procedurally generated geometry and textures (no external resource files)
*
*   NOTE: This is the same clip-space decal projection idea used by models_decals.c
*   (in turn based on three.js' DecalGeometry), applied here to a generated mesh and
*   generated textures instead of a loaded model, so the example is fully self-contained
*
*   Example complexity rating: [★★★★] 4/4
*
*   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"
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"
#include "raymath.h"

#include <stddef.h>     // Required for: NULL
#include <string.h>     // Required for: memcpy()
#include <math.h>       // Required for: fabsf(), fminf()

//--------------------------------------------------------------------------------------
// Global Definitions
//--------------------------------------------------------------------------------------
#define MAX_DECALS  256

//--------------------------------------------------------------------------------------
// Types and Structures Definition
//--------------------------------------------------------------------------------------

// Growable triangle-soup buffer used while building a clipped decal mesh
typedef struct MeshBuilder {
    int vertexCount;
    int vertexCapacity;
    Vector3 *vertices;
    Vector2 *uvs;
} MeshBuilder;

//------------------------------------------------------------------------------------
// Module Functions Declaration
//------------------------------------------------------------------------------------
static void AddTriangleToMeshBuilder(MeshBuilder *mb, Vector3 v0, Vector3 v1, Vector3 v2);
static void FreeMeshBuilder(MeshBuilder *mb);
static Mesh BuildMesh(MeshBuilder *mb);
static Vector3 ClipSegment(Vector3 v0, Vector3 v1, Vector3 p, float s);
static Mesh GenMeshDecal(Model model, Matrix projection, float decalSize, float decalOffset);

//------------------------------------------------------------------------------------
// 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 [models] example - procedural decals");

    Camera3D camera = { 0 };
    camera.position = (Vector3){ 0.0f, 2.5f, 5.0f };
    camera.target = (Vector3){ 0.0f, 0.0f, 0.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 45.0f;
    camera.projection = CAMERA_PERSPECTIVE;

    // Target model: a procedurally generated knot with a checkerboard skin
    Model model = LoadModelFromMesh(GenMeshTorus(0.8f, 1.8f, 32, 64));

    Image checkerImage = GenImageChecked(512, 512, 32, 32, LIGHTGRAY, GRAY);
    Texture2D modelTexture = LoadTextureFromImage(checkerImage);
    UnloadImage(checkerImage);
    SetTextureFilter(modelTexture, TEXTURE_FILTER_BILINEAR);
    model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = modelTexture;

    BoundingBox modelBBox = GetMeshBoundingBox(model.meshes[0]);
    camera.target = Vector3Lerp(modelBBox.min, modelBBox.max, 0.5f);

    float modelSize = fminf(fminf(fabsf(modelBBox.max.x - modelBBox.min.x),
        fabsf(modelBBox.max.y - modelBBox.min.y)), fabsf(modelBBox.max.z - modelBBox.min.z));

    float decalSize = modelSize*0.35f;
    float decalOffset = 0.01f;

    // Translucent cube previewing where the next decal will land
    Model placementCube = LoadModelFromMesh(GenMeshCube(decalSize, decalSize, decalSize));
    placementCube.materials[0].maps[0].color = LIME;

    // Decal texture: a procedurally drawn target/bullseye, no image file needed
    Image decalImage = GenImageColor(128, 128, BLANK);
    ImageDrawCircle(&decalImage, 64, 64, 60, RED);
    ImageDrawCircle(&decalImage, 64, 64, 45, WHITE);
    ImageDrawCircle(&decalImage, 64, 64, 30, RED);
    ImageDrawCircle(&decalImage, 64, 64, 15, YELLOW);

    Texture2D decalTexture = LoadTextureFromImage(decalImage);
    UnloadImage(decalImage);
    SetTextureFilter(decalTexture, TEXTURE_FILTER_BILINEAR);

    Material decalMaterial = LoadMaterialDefault();
    decalMaterial.maps[MATERIAL_MAP_DIFFUSE].texture = decalTexture;
    decalMaterial.maps[MATERIAL_MAP_DIFFUSE].color = RAYWHITE;

    bool showModel = true;
    Model decals[MAX_DECALS] = { 0 };
    int decalCount = 0;

    SetTargetFPS(60);
    //--------------------------------------------------------------------------------------

    // Main game loop
    while (!WindowShouldClose())    // Detect window close button or ESC key
    {
        // Update
        //----------------------------------------------------------------------------------
        if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) UpdateCamera(&camera, CAMERA_THIRD_PERSON);

        // Cast a ray from the mouse and keep the closest point it hits on the model
        Ray ray = GetScreenToWorldRay(GetMousePosition(), camera);
        RayCollision collision = { 0 };

        if (GetRayCollisionBox(ray, modelBBox).hit && (decalCount < MAX_DECALS))
        {
            for (int m = 0; m < model.meshCount; m++)
            {
                RayCollision meshHit = GetRayCollisionMesh(ray, model.meshes[m], model.transform);
                if (meshHit.hit && (!collision.hit || (meshHit.distance < collision.distance))) collision = meshHit;
            }
        }

        // Project a new decal at the hit point, facing along the surface normal
        if (collision.hit && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && (decalCount < MAX_DECALS))
        {
            Vector3 lookTarget = Vector3Add(collision.point, collision.normal);
            Matrix splat = MatrixLookAt(collision.point, lookTarget, (Vector3){ 0.0f, 1.0f, 0.0f });
            splat = MatrixMultiply(splat, MatrixRotateZ(DEG2RAD*(float)GetRandomValue(-180, 180)));

            Mesh decalMesh = GenMeshDecal(model, splat, decalSize, decalOffset);

            if (decalMesh.vertexCount > 0)
            {
                decals[decalCount] = LoadModelFromMesh(decalMesh);
                decals[decalCount].materials[0].maps[0] = decalMaterial.maps[0];
                decalCount++;
            }
        }
        //----------------------------------------------------------------------------------

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();

            ClearBackground(RAYWHITE);

            BeginMode3D(camera);

                if (showModel) DrawModel(model, Vector3Zero(), 1.0f, WHITE);

                for (int i = 0; i < decalCount; i++) DrawModel(decals[i], Vector3Zero(), 1.0f, WHITE);

                // Preview cube at the surface point currently under the mouse
                if (collision.hit)
                {
                    Vector3 lookTarget = Vector3Add(collision.point, collision.normal);
                    Matrix splat = MatrixLookAt(collision.point, lookTarget, (Vector3){ 0.0f, 1.0f, 0.0f });
                    placementCube.transform = MatrixInvert(splat);
                    DrawModel(placementCube, Vector3Zero(), 1.0f, Fade(WHITE, 0.5f));
                }

                DrawGrid(10, 1.0f);

            EndMode3D();

            // Vertex/triangle counts: base model vs total once decal geometry is added
            int baseVertices = 0, baseTriangles = 0;
            for (int i = 0; i < model.meshCount; i++)
            {
                baseVertices += model.meshes[i].vertexCount;
                baseTriangles += model.meshes[i].triangleCount;
            }

            int totalVertices = baseVertices, totalTriangles = baseTriangles;
            for (int i = 0; i < decalCount; i++)
            {
                totalVertices += decals[i].meshes[0].vertexCount;
                totalTriangles += decals[i].meshes[0].triangleCount;
            }

            int statX = screenWidth - 280;
            DrawText("Vertices", statX + 90, 10, 10, LIME);
            DrawText("Triangles", statX + 180, 10, 10, LIME);
            DrawText("Base Model", statX, 25, 10, LIME);
            DrawText(TextFormat("%d", baseVertices), statX + 90, 25, 10, LIME);
            DrawText(TextFormat("%d", baseTriangles), statX + 180, 25, 10, LIME);
            DrawText("TOTAL", statX, 40, 10, LIME);
            DrawText(TextFormat("%d", totalVertices), statX + 90, 40, 10, LIME);
            DrawText(TextFormat("%d", totalTriangles), statX + 180, 40, 10, LIME);

            if (GuiButton((Rectangle){ 10, screenHeight - 80.0f, 100, 40 }, showModel ? "Hide Model" : "Show Model"))
                showModel = !showModel;

            if (GuiButton((Rectangle){ 120, screenHeight - 80.0f, 100, 40 }, "Clear Decals"))
            {
                for (int i = 0; i < decalCount; i++) UnloadModel(decals[i]);
                decalCount = 0;
            }

            DrawText("Left click: place decal   |   Right drag: orbit camera", 10, screenHeight - 25, 10, DARKGRAY);
            DrawFPS(10, 10);

        EndDrawing();
        //----------------------------------------------------------------------------------
    }

    // De-Initialization
    //--------------------------------------------------------------------------------------
    for (int i = 0; i < decalCount; i++) UnloadModel(decals[i]);

    UnloadModel(placementCube);
    UnloadTexture(decalTexture);
    UnloadTexture(modelTexture);
    UnloadModel(model);

    CloseWindow();        // Close window and OpenGL context
    //--------------------------------------------------------------------------------------

    return 0;
}

//----------------------------------------------------------------------------------
// Module Functions Definition
//----------------------------------------------------------------------------------

// Appends a triangle to a growable mesh builder buffer, growing it in fixed-size chunks
static void AddTriangleToMeshBuilder(MeshBuilder *mb, Vector3 v0, Vector3 v1, Vector3 v2)
{
    if (mb->vertexCapacity <= (mb->vertexCount + 3))
    {
        int newVertexCapacity = (1 + (mb->vertexCapacity/256))*256;
        Vector3 *newVertices = (Vector3 *)MemAlloc(newVertexCapacity*sizeof(Vector3));

        if (mb->vertexCapacity > 0)
        {
            memcpy(newVertices, mb->vertices, mb->vertexCount*sizeof(Vector3));
            MemFree(mb->vertices);
        }

        mb->vertices = newVertices;
        mb->vertexCapacity = newVertexCapacity;
    }

    int index = mb->vertexCount;
    mb->vertexCount += 3;

    mb->vertices[index + 0] = v0;
    mb->vertices[index + 1] = v1;
    mb->vertices[index + 2] = v2;
}

// Frees a mesh builder's backing buffers
static void FreeMeshBuilder(MeshBuilder *mb)
{
    if (mb->vertices != NULL) MemFree(mb->vertices);
    if (mb->uvs != NULL) MemFree(mb->uvs);
    *mb = (MeshBuilder){ 0 };
}

// Converts a mesh builder's triangle soup into a standard uploaded raylib Mesh
static Mesh BuildMesh(MeshBuilder *mb)
{
    Mesh outMesh = { 0 };

    outMesh.vertexCount = mb->vertexCount;
    outMesh.triangleCount = mb->vertexCount/3;
    outMesh.vertices = (float *)MemAlloc(outMesh.vertexCount*3*sizeof(float));
    if (mb->uvs != NULL) outMesh.texcoords = (float *)MemAlloc(outMesh.vertexCount*2*sizeof(float));

    for (int i = 0; i < mb->vertexCount; i++)
    {
        outMesh.vertices[3*i + 0] = mb->vertices[i].x;
        outMesh.vertices[3*i + 1] = mb->vertices[i].y;
        outMesh.vertices[3*i + 2] = mb->vertices[i].z;

        if (mb->uvs != NULL)
        {
            outMesh.texcoords[2*i + 0] = mb->uvs[i].x;
            outMesh.texcoords[2*i + 1] = mb->uvs[i].y;
        }
    }

    UploadMesh(&outMesh, false);

    return outMesh;
}

// Clips the segment [v0, v1] against the plane p.x = s, returning the intersection point
static Vector3 ClipSegment(Vector3 v0, Vector3 v1, Vector3 p, float s)
{
    float d0 = Vector3DotProduct(v0, p) - s;
    float d1 = Vector3DotProduct(v1, p) - s;
    float t = d0/(d0 - d1);

    return Vector3Lerp(v0, v1, t);
}

// Builds a decal mesh: the model's triangles are transformed into the decal's local space
// (so the decal sits at the origin, facing +Z) and clipped against a decalSize-sided box,
// following the same clip-space projection idea used by engines' decal systems (and by
// three.js' DecalGeometry, which the technique is commonly traced back to). What's left
// after clipping becomes the decal's own small mesh, UV-mapped from its local coordinates
static Mesh GenMeshDecal(Model model, Matrix projection, float decalSize, float decalOffset)
{
    Matrix invProj = MatrixInvert(projection);
    MeshBuilder meshBuilders[2] = { 0 };
    int mbIndex = 0;

    // Gather triangles that land anywhere near the decal box; this is a loose, cheap
    // pre-filter meant to skip most of the model, the precise clip happens below
    for (int meshIndex = 0; meshIndex < model.meshCount; meshIndex++)
    {
        Mesh mesh = model.meshes[meshIndex];

        for (int tri = 0; tri < mesh.triangleCount; tri++)
        {
            Vector3 vertices[3] = { 0 };

            if (mesh.indices == NULL)
            {
                for (int v = 0; v < 3; v++)
                {
                    vertices[v] = (Vector3){
                        mesh.vertices[3*3*tri + 3*v + 0],
                        mesh.vertices[3*3*tri + 3*v + 1],
                        mesh.vertices[3*3*tri + 3*v + 2]
                    };
                }
            }
            else
            {
                for (int v = 0; v < 3; v++)
                {
                    int idx = mesh.indices[3*tri + v];
                    vertices[v] = (Vector3){
                        mesh.vertices[3*idx + 0],
                        mesh.vertices[3*idx + 1],
                        mesh.vertices[3*idx + 2]
                    };
                }
            }

            int insideCount = 0;
            for (int i = 0; i < 3; i++)
            {
                Vector3 v = Vector3Transform(vertices[i], projection);
                if ((fabsf(v.x) < decalSize) || (fabsf(v.y) <= decalSize) || (fabsf(v.z) <= decalSize)) insideCount++;
                vertices[i] = v;
            }

            if (insideCount > 0) AddTriangleToMeshBuilder(&meshBuilders[mbIndex], vertices[0], vertices[1], vertices[2]);
        }
    }

    // Clip the surviving triangles against each of the decal box's 6 faces in turn
    Vector3 planes[6] = {
        {  1,  0,  0 }, { -1,  0,  0 },
        {  0,  1,  0 }, {  0, -1,  0 },
        {  0,  0,  1 }, {  0,  0, -1 }
    };

    for (int face = 0; face < 6; face++)
    {
        mbIndex = 1 - mbIndex;

        MeshBuilder *inMesh = &meshBuilders[1 - mbIndex];
        MeshBuilder *outMesh = &meshBuilders[mbIndex];

        outMesh->vertexCount = 0;
        float s = 0.5f*decalSize;

        for (int i = 0; i < inMesh->vertexCount; i += 3)
        {
            Vector3 nV1, nV2, nV3, nV4;

            float d1 = Vector3DotProduct(inMesh->vertices[i + 0], planes[face]) - s;
            float d2 = Vector3DotProduct(inMesh->vertices[i + 1], planes[face]) - s;
            float d3 = Vector3DotProduct(inMesh->vertices[i + 2], planes[face]) - s;

            int v1Out = (d1 > 0);
            int v2Out = (d2 > 0);
            int v3Out = (d3 > 0);
            int total = v1Out + v2Out + v3Out;

            switch (total)
            {
                case 0:
                    // Whole triangle is inside this face, keep it as-is
                    AddTriangleToMeshBuilder(outMesh, inMesh->vertices[i], inMesh->vertices[i + 1], inMesh->vertices[i + 2]);
                    break;
                case 1:
                    // One corner is outside; clip it off, turning the triangle into a quad
                    if (v1Out)
                    {
                        nV1 = inMesh->vertices[i + 1];
                        nV2 = inMesh->vertices[i + 2];
                        nV3 = ClipSegment(inMesh->vertices[i], nV1, planes[face], s);
                        nV4 = ClipSegment(inMesh->vertices[i], nV2, planes[face], s);
                    }

                    if (v2Out)
                    {
                        nV1 = inMesh->vertices[i];
                        nV2 = inMesh->vertices[i + 2];
                        nV3 = ClipSegment(inMesh->vertices[i + 1], nV1, planes[face], s);
                        nV4 = ClipSegment(inMesh->vertices[i + 1], nV2, planes[face], s);

                        AddTriangleToMeshBuilder(outMesh, nV3, nV2, nV1);
                        AddTriangleToMeshBuilder(outMesh, nV2, nV3, nV4);
                        break;
                    }

                    if (v3Out)
                    {
                        nV1 = inMesh->vertices[i];
                        nV2 = inMesh->vertices[i + 1];
                        nV3 = ClipSegment(inMesh->vertices[i + 2], nV1, planes[face], s);
                        nV4 = ClipSegment(inMesh->vertices[i + 2], nV2, planes[face], s);
                    }

                    AddTriangleToMeshBuilder(outMesh, nV1, nV2, nV3);
                    AddTriangleToMeshBuilder(outMesh, nV4, nV3, nV2);
                    break;
                case 2:
                    // Two corners are outside; only the small corner near the surviving vertex remains
                    if (!v1Out)
                    {
                        nV1 = inMesh->vertices[i];
                        nV2 = ClipSegment(nV1, inMesh->vertices[i + 1], planes[face], s);
                        nV3 = ClipSegment(nV1, inMesh->vertices[i + 2], planes[face], s);
                        AddTriangleToMeshBuilder(outMesh, nV1, nV2, nV3);
                    }

                    if (!v2Out)
                    {
                        nV1 = inMesh->vertices[i + 1];
                        nV2 = ClipSegment(nV1, inMesh->vertices[i + 2], planes[face], s);
                        nV3 = ClipSegment(nV1, inMesh->vertices[i], planes[face], s);
                        AddTriangleToMeshBuilder(outMesh, nV1, nV2, nV3);
                    }

                    if (!v3Out)
                    {
                        nV1 = inMesh->vertices[i + 2];
                        nV2 = ClipSegment(nV1, inMesh->vertices[i], planes[face], s);
                        nV3 = ClipSegment(nV1, inMesh->vertices[i + 1], planes[face], s);
                        AddTriangleToMeshBuilder(outMesh, nV1, nV2, nV3);
                    }
                    break;
                default:
                    // All 3 corners are outside this face, the triangle is fully clipped away
                    break;
            }
        }
    }

    MeshBuilder *finalMesh = &meshBuilders[mbIndex];
    Mesh decalMesh = { 0 };

    if (finalMesh->vertexCount > 0)
    {
        finalMesh->uvs = (Vector2 *)MemAlloc(sizeof(Vector2)*finalMesh->vertexCount);

        for (int i = 0; i < finalMesh->vertexCount; i++)
        {
            // Clipped coordinates run roughly (-decalSize/2 .. decalSize/2); remap to (0..1)
            finalMesh->uvs[i].x = (finalMesh->vertices[i].x/decalSize + 0.5f);
            finalMesh->uvs[i].y = (finalMesh->vertices[i].y/decalSize + 0.5f);

            // Nudge slightly along the normal so the decal doesn't z-fight with the surface
            finalMesh->vertices[i].z -= decalOffset;
            finalMesh->vertices[i] = Vector3Transform(finalMesh->vertices[i], invProj);
        }

        decalMesh = BuildMesh(finalMesh);
    }

    FreeMeshBuilder(&meshBuilders[0]);
    FreeMeshBuilder(&meshBuilders[1]);

    return decalMesh;
}

From raylib 6.2. The original on GitHub.