← ziglang.inraylib-zigGitHub

decals

★★★★by JP Mortiboys (@themushroompirates)

There is no browser build of this example yet.

Is it the same as C?

//! raylib [models] example - decals
//!
//! Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5)
//! Based on previous work by @mrdoob
//!
//! Zig-API translation of examples/models/models_decals.c (raylib 6.2).
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2025 JP Mortiboys (@themushroompirates) and Ramon Santamaria (@raysan5)

const std = @import("std");
const rl = @import("raylib");

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 - decals");
    defer rl.closeWindow(); // Close window and OpenGL context

    // Load character model
    const model = try rl.loadModel("resources/models/obj/character.obj");
    defer rl.unloadModel(model);

    // Apply character skin
    const model_texture = try rl.loadTexture("resources/models/obj/character_diffuse.png");
    defer rl.unloadTexture(model_texture);
    rl.setTextureFilter(model_texture, .bilinear);
    model.materials.?[0].map(.diffuse).texture = model_texture;

    const model_bbox = rl.getMeshBoundingBox(model.meshes.?[0]); // Get mesh bounding box

    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),
    );

    // Define the camera to look into our 3d world, framing the model
    var camera: rl.Camera = .{
        .position = .{ .x = 0.0, .y = model_bbox.max.y * 1.2, .z = model_size * 3.0 }, // Camera position
        .target = rl.math.vector3Lerp(model_bbox.min, model_bbox.max, 0.5), // Camera looking at point
        .up = .{ .x = 0.0, .y = 1.6, .z = 0.0 }, // Camera up vector (rotation towards target)
        .fovy = 45.0, // Camera field-of-view Y
        .projection = .perspective, // Camera projection type
    };

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

    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;

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

    var decal_image = try rl.loadImage("resources/raylib_logo.png");
    rl.imageResizeNN(&decal_image, @divTrunc(decal_image.width, 4), @divTrunc(decal_image.height, 4));
    const decal_texture = try rl.loadTextureFromImage(decal_image);
    rl.unloadImage(decal_image);

    rl.setTextureFilter(decal_texture, .bilinear);
    decal_material.map(.diffuse).texture = decal_texture;
    decal_material.map(.diffuse).color = .ray_white;

    var show_model = true;
    var decal_models: [max_decals]rl.Model = undefined;
    var decal_count: usize = 0;
    // Unload decal models
    defer for (decal_models[0..decal_count]) |decal_model| rl.unloadModel(decal_model);

    var decal_builder: DecalBuilder = .{};
    defer decal_builder.deinit(gpa); // Free the data for decal generation

    rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second

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

        // Display information about closest hit
        var collision: rl.RayCollision = .{
            .hit = false,
            .distance = std.math.floatMax(f32),
            .point = .{ .x = 0, .y = 0, .z = 0 },
            .normal = .{ .x = 0, .y = 0, .z = 0 },
        };

        // Get mouse ray
        const ray = rl.getScreenToWorldRay(rl.getMousePosition(), camera);

        // Check ray collision against bounding box first, before trying the full ray-mesh test
        const box_hit_info = rl.getRayCollisionBox(ray, model_bbox);

        if (box_hit_info.hit and decal_count < max_decals) {
            // Check ray collision against model meshes
            var mesh_hit_info = std.mem.zeroes(rl.RayCollision);
            for (model.meshSlice()) |mesh| {
                // NOTE: We consider the model.transform for the collision check but
                // it can be checked against any transform Matrix, used when checking against same
                // model drawn multiple times with multiple transforms
                mesh_hit_info = rl.getRayCollisionMesh(ray, mesh, model.transform);
                // Save the closest hit mesh
                if (mesh_hit_info.hit and (!collision.hit or collision.distance > mesh_hit_info.distance)) {
                    collision = mesh_hit_info;
                }
            }

            if (mesh_hit_info.hit) collision = mesh_hit_info;
        }

        // Add decal to mesh on hit point
        if (collision.hit and rl.isMouseButtonPressed(.left) and decal_count < max_decals) {
            // Create the transformation to project the decal
            const origin = rl.math.vector3Add(collision.point, rl.math.vector3Scale(collision.normal, 1.0));
            var splat = rl.math.matrixLookAt(collision.point, origin, .{ .x = 0.0, .y = 1.0, .z = 0.0 });

            // Spin the placement around a bit
            splat = rl.math.matrixMultiply(splat, rl.math.matrixRotateZ(rl.deg2rad * @as(f32, @floatFromInt(rl.getRandomValue(-180, 180)))));

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

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

        rl.clearBackground(.ray_white);

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

            const zero: rl.Vector3 = .{ .x = 0.0, .y = 0.0, .z = 0.0 };

            // Draw the model at the origin and default scale
            if (show_model) rl.drawModel(model, zero, 1.0, .white);

            // Draw the decal models
            for (decal_models[0..decal_count]) |decal_model| rl.drawModel(decal_model, zero, 1.0, .white);

            // If we hit the mesh, draw the box for the decal
            if (collision.hit) {
                const origin = rl.math.vector3Add(collision.point, rl.math.vector3Scale(collision.normal, 1.0));
                const splat = rl.math.matrixLookAt(collision.point, origin, .{ .x = 0, .y = 1, .z = 0 });
                placement_cube.transform = rl.math.matrixInvert(splat);
                rl.drawModel(placement_cube, zero, 1.0, rl.fade(.white, 0.5));
            }

            rl.drawGrid(10, 10.0);
        }

        var y_pos: f32 = 10;
        const x0 = @as(f32, @floatFromInt(rl.getScreenWidth())) - 300.0;
        const x1 = x0 + 100;
        const x2 = x1 + 100;

        drawCell("Vertices", x1, y_pos);
        drawCell("Triangles", x2, y_pos);
        y_pos += 15;

        var vertex_count: i32 = 0;
        var triangle_count: i32 = 0;

        for (model.meshSlice()) |mesh| {
            vertex_count += mesh.vertexCount;
            triangle_count += mesh.triangleCount;
        }

        drawCell("Main model", x0, y_pos);
        drawCell(rl.textFormat("{d}", .{vertex_count}), x1, y_pos);
        drawCell(rl.textFormat("{d}", .{triangle_count}), x2, y_pos);
        y_pos += 15;

        for (decal_models[0..decal_count], 0..) |decal_model, i| {
            const mesh = decal_model.meshes.?[0];

            if (i == 20) {
                drawCell("...", x0, y_pos);
                y_pos += 15;
            }

            if (i < 20) {
                drawCell(rl.textFormat("Decal #{d}", .{i + 1}), x0, y_pos);
                drawCell(rl.textFormat("{d}", .{mesh.vertexCount}), x1, y_pos);
                drawCell(rl.textFormat("{d}", .{mesh.triangleCount}), x2, y_pos);
                y_pos += 15;
            }

            vertex_count += mesh.vertexCount;
            triangle_count += mesh.triangleCount;
        }

        drawCell("TOTAL", x0, y_pos);
        drawCell(rl.textFormat("{d}", .{vertex_count}), x1, y_pos);
        drawCell(rl.textFormat("{d}", .{triangle_count}), x2, y_pos);
        y_pos += 15;

        rl.drawText("Hold RMB to move camera", 10, 430, 10, .gray);
        rl.drawText("(c) Character model and texture from kenney.nl", screen_width - 260, screen_height - 20, 10, .gray);

        // UI elements
        if (guiButton(.{ .x = 10, .y = screen_height - 100.0, .width = 100, .height = 60 }, if (show_model) "Hide Model" else "Show Model")) {
            show_model = !show_model;
        }

        if (guiButton(.{ .x = 10 + 110, .y = screen_height - 100.0, .width = 100, .height = 60 }, "Clear Decals")) {
            // Clear decals, unload all decal models
            for (decal_models[0..decal_count]) |decal_model| rl.unloadModel(decal_model);
            decal_count = 0;
        }

        rl.drawFPS(10, 10);
    }
}

/// Builds decal meshes by clipping a model's triangles against the decal box.
/// Holds two triangle lists (vertices in groups of three), reused across calls:
/// each clipping pass reads from one and writes to the other.
const DecalBuilder = struct {
    lists: [2]std.ArrayList(rl.Vector3) = .{ .empty, .empty },

    fn deinit(builder: *DecalBuilder, gpa: std.mem.Allocator) void {
        for (&builder.lists) |*list| list.deinit(gpa);
    }

    /// Generate mesh decals for provided model; null when the decal covers no triangle.
    fn genMesh(
        builder: *DecalBuilder,
        gpa: std.mem.Allocator,
        target: rl.Model,
        projection: rl.Matrix,
        decal_size: f32,
        decal_offset: f32,
    ) !?rl.Mesh {
        // We're going to need the inverse matrix
        const inv_proj = rl.math.matrixInvert(projection);

        // Reset the triangle lists
        for (&builder.lists) |*list| list.clearRetainingCapacity();

        // We'll be flip-flopping between the two lists
        // Reading from one and writing to the other, then swapping
        var list_index: usize = 0;

        // First pass, just get any triangle inside the bounding box (for each mesh of the model)
        for (target.meshSlice()) |mesh| {
            const vertices = mesh.vertices.?;
            for (0..@intCast(mesh.triangleCount)) |tri| {
                var triangle: [3]rl.Vector3 = undefined;

                // The way we calculate the vertices of the mesh triangle
                // depend on whether the mesh vertices are indexed or not
                if (mesh.indices) |indices| {
                    for (&triangle, 0..) |*vertex, v| {
                        vertex.* = .{
                            .x = vertices[3 * @as(usize, indices[3 * tri + 0]) + v],
                            .y = vertices[3 * @as(usize, indices[3 * tri + 1]) + v],
                            .z = vertices[3 * @as(usize, indices[3 * tri + 2]) + v],
                        };
                    }
                } else {
                    for (&triangle, 0..) |*vertex, v| {
                        vertex.* = .{
                            .x = vertices[3 * 3 * tri + 3 * v + 0],
                            .y = vertices[3 * 3 * tri + 3 * v + 1],
                            .z = vertices[3 * 3 * tri + 3 * v + 2],
                        };
                    }
                }

                // Transform all 3 vertices of the triangle
                // and check if they are inside our decal box
                var inside_count: usize = 0;
                for (&triangle) |*vertex| {
                    // To projection space
                    const v = rl.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;

                    // We need to keep the transformed vertex
                    vertex.* = v;
                }

                // If any of them are inside, we add the triangle - we'll clip it later
                if (inside_count > 0) try builder.lists[list_index].appendSlice(gpa, &triangle);
            }
        }

        // Clipping time! We need to clip against all 6 directions
        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| {
            // Swap current list (so we read from the one we just wrote to)
            list_index = 1 - list_index;

            const in = builder.lists[1 - list_index].items;
            const out = &builder.lists[list_index];

            // Reset write list
            out.clearRetainingCapacity();

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

                // Calculate, how many vertices of the face lie outside of the clipping plane
                var outside: [3]bool = undefined;
                for (v, &outside) |vertex, *is_out| is_out.* = rl.math.vector3DotProduct(vertex, plane) - s > 0;
                const total = @as(u8, @intFromBool(outside[0])) + @intFromBool(outside[1]) + @intFromBool(outside[2]);

                switch (total) {
                    // The entire face lies inside of the plane, no clipping needed
                    0 => try out.appendSlice(gpa, &v),
                    // One vertex lies outside of the plane, perform clipping
                    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 });
                        try out.appendSlice(gpa, &.{ 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 });
                        try out.appendSlice(gpa, &.{ 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 });
                        try out.appendSlice(gpa, &.{ n4, n3, n2 });
                    },
                    // Two vertices lie outside of the plane: keep the inside one,
                    // clipping its edges towards the next two vertices (in winding order)
                    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 });
                    },
                    // The entire face lies outside of the plane, so let's discard the corresponding vertices
                    else => {},
                }
            }
        }

        // Now we just need to re-transform the vertices
        const decal = builder.lists[list_index].items;

        // Return no mesh when there's nothing to add
        if (decal.len == 0) return null;

        // Decal model data ready, create the 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| {
            // Calculate the UVs based on the projected coords
            // They are clipped to (-decal_size .. decal_size) and we want them (0..1)
            texcoords[2 * i + 0] = vertex.x / decal_size + 0.5;
            texcoords[2 * i + 1] = vertex.y / decal_size + 0.5;

            // Tiny nudge in the normal direction so it renders properly over the mesh
            var nudged = vertex;
            nudged.z -= decal_offset;

            // From projection space to world space
            const world = rl.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;
    }
};

/// Clip segment
fn clipSegment(v0: rl.Vector3, v1: rl.Vector3, p: rl.Vector3, s: f32) rl.Vector3 {
    const d0 = rl.math.vector3DotProduct(v0, p) - s;
    const d1 = rl.math.vector3DotProduct(v1, p) - s;
    const s0 = d0 / (d0 - d1);

    return rl.math.vector3Lerp(v0, v1, s0);
}

/// Draw one cell of the vertex/triangle count table
fn drawCell(text: [:0]const u8, x: f32, y: f32) void {
    rl.drawText(text, @intFromFloat(x), @intFromFloat(y), 10, .lime);
}

/// Button UI element
fn guiButton(rec: rl.Rectangle, label: [:0]const u8) bool {
    var bg_color: rl.Color = .gray;
    var pressed = false;

    if (rl.checkCollisionPointRec(rl.getMousePosition(), rec)) {
        bg_color = .light_gray;
        pressed = rl.isMouseButtonPressed(.left);
    }

    rl.drawRectangleRec(rec, bg_color);
    rl.drawRectangleLinesEx(rec, 2.0, .dark_gray);

    const font_size = 10;
    const text_width: f32 = @floatFromInt(rl.measureText(label, font_size));

    rl.drawText(label, @intFromFloat(rec.x + rec.width * 0.5 - text_width * 0.5), @intFromFloat(rec.y + rec.height * 0.5 - font_size * 0.5), font_size, .dark_gray);

    return pressed;
}
/*******************************************************************************************
*
*   raylib [models] example - decals
*
*   Example complexity rating: [★★★★] 4/4
*
*   Example originally created with raylib 6.0, last time updated with raylib 6.0
*
*   Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5)
*   Based on previous work by @mrdoob
*
*   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 JP Mortiboys (@themushroompirates) and Ramon Santamaria (@raysan5)
*
********************************************************************************************/

#include "raylib.h"

#include "raymath.h"

#include <string.h>         // Required for: memcpy()

#undef FLT_MAX
#define FLT_MAX     340282346638528859811704183484516925440.0f     // Maximum value of a float, from bit pattern 01111111011111111111111111111111

#define MAX_DECALS 256

//----------------------------------------------------------------------------------
// Types and Structures Definition
//----------------------------------------------------------------------------------
typedef struct MeshBuilder {
    int vertexCount;
    int vertexCapacity;
    Vector3 *vertices;
    Vector2 *uvs;
} MeshBuilder;

//------------------------------------------------------------------------------------
// Module Functions Declaration
//------------------------------------------------------------------------------------
static void AddTriangleToMeshBuilder(MeshBuilder *mb, Vector3 vertices[3]);
static void FreeMeshBuilder(MeshBuilder *mb);
static Mesh BuildMesh(MeshBuilder *mb);
static Mesh GenMeshDecal(Model inputModel, Matrix projection, float decalSize, float decalOffset);
static Vector3 ClipSegment(Vector3 v0, Vector3 v1, Vector3 p, float s);
static void FreeDecalMeshData(void) { GenMeshDecal((Model){ .meshCount = -1 }, (Matrix){ 0 }, 0.0f, 0.0f); }
static bool GuiButton(Rectangle rec, const char *label);

//------------------------------------------------------------------------------------
// 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 - decals");

    // Define the camera to look into our 3d world
    Camera camera = { 0 };
    camera.position = (Vector3){ 5.0f, 5.0f, 5.0f }; // Camera position
    camera.target = (Vector3){ 0.0f, 1.0f, 0.0f };      // Camera looking at point
    camera.up = (Vector3){ 0.0f, 1.6f, 0.0f };          // Camera up vector (rotation towards target)
    camera.fovy = 45.0f;                                // Camera field-of-view Y
    camera.projection = CAMERA_PERSPECTIVE;             // Camera projection type

    // Load character model
    Model model = LoadModel("resources/models/obj/character.obj");

    // Apply character skin
    Texture2D modelTexture = LoadTexture("resources/models/obj/character_diffuse.png");
    SetTextureFilter(modelTexture, TEXTURE_FILTER_BILINEAR);
    model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = modelTexture;

    BoundingBox modelBBox = GetMeshBoundingBox(model.meshes[0]);    // Get mesh bounding box

    camera.target  = Vector3Lerp(modelBBox.min, modelBBox.max, 0.5f);
    camera.position = Vector3Scale(modelBBox.max, 1.0f);
    camera.position.x *= 0.1f;

    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));

    camera.position = (Vector3){ 0.0f, modelBBox.max.y*1.2f, modelSize*3.0f };

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

    Model placementCube = LoadModelFromMesh(GenMeshCube(decalSize, decalSize, decalSize));
    placementCube.materials[0].maps[0].color = LIME;

    Material decalMaterial = LoadMaterialDefault();
    decalMaterial.maps[0].color = YELLOW;

    Image decalImage = LoadImage("resources/raylib_logo.png");
    ImageResizeNN(&decalImage, decalImage.width/4, decalImage.height/4);
    Texture decalTexture = LoadTextureFromImage(decalImage);
    UnloadImage(decalImage);

    SetTextureFilter(decalTexture, TEXTURE_FILTER_BILINEAR);
    decalMaterial.maps[MATERIAL_MAP_DIFFUSE].texture = decalTexture;
    decalMaterial.maps[MATERIAL_MAP_DIFFUSE].color = RAYWHITE;

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

    SetTargetFPS(60);                   // Set our game to run at 60 frames-per-second
    //--------------------------------------------------------------------------------------

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

        // Display information about closest hit
        RayCollision collision = { 0 };
        collision.distance = FLT_MAX;
        collision.hit = false;

        // Get mouse ray
        Ray ray = GetScreenToWorldRay(GetMousePosition(), camera);

        // Check ray collision against bounding box first, before trying the full ray-mesh test
        RayCollision boxHitInfo = GetRayCollisionBox(ray, modelBBox);

        if ((boxHitInfo.hit) && (decalCount < MAX_DECALS))
        {
            // Check ray collision against model meshes
            RayCollision meshHitInfo = { 0 };
            for (int m = 0; m < model.meshCount; m++)
            {
                // NOTE: We consider the model.transform for the collision check but
                // it can be checked against any transform Matrix, used when checking against same
                // model drawn multiple times with multiple transforms
                meshHitInfo = GetRayCollisionMesh(ray, model.meshes[m], model.transform);
                if (meshHitInfo.hit)
                {
                    // Save the closest hit mesh
                    if (!collision.hit || (collision.distance > meshHitInfo.distance)) collision = meshHitInfo;
                }
            }

            if (meshHitInfo.hit) collision = meshHitInfo;
        }

        // Add decal to mesh on hit point
        if (collision.hit && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && (decalCount < MAX_DECALS))
        {
            // Create the transformation to project the decal
            Vector3 origin = Vector3Add(collision.point, Vector3Scale(collision.normal, 1.0f));
            Matrix splat = MatrixLookAt(collision.point, origin, (Vector3){ 0.0f, 1.0f, 0.0f });

            // Spin the placement around a bit
            splat = MatrixMultiply(splat, MatrixRotateZ(DEG2RAD*((float)GetRandomValue(-180, 180))));

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

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

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();
            ClearBackground(RAYWHITE);

            BeginMode3D(camera);
                // Draw the model at the origin and default scale
                if (showModel) DrawModel(model, (Vector3){0.0f, 0.0f, 0.0f}, 1.0f, WHITE);

                // Draw the decal models
                for (int i = 0; i < decalCount; i++) DrawModel(decalModels[i], (Vector3){ 0 }, 1.0f, WHITE);

                // If we hit the mesh, draw the box for the decal
                if (collision.hit)
                {
                    Vector3 origin = Vector3Add(collision.point, Vector3Scale(collision.normal, 1.0f));
                    Matrix splat = MatrixLookAt(collision.point, origin, (Vector3){0,1,0});
                    placementCube.transform = MatrixInvert(splat);
                    DrawModel(placementCube, (Vector3){ 0 }, 1.0f, Fade(WHITE, 0.5f));
                }

                DrawGrid(10, 10.0f);
            EndMode3D();

            float yPos = 10;
            float x0 = GetScreenWidth() - 300.0f;
            float x1 = x0 + 100;
            float x2 = x1 + 100;

            DrawText("Vertices", (int)x1, (int)yPos, 10, LIME);
            DrawText("Triangles", (int)x2, (int)yPos, 10, LIME);
            yPos += 15;

            int vertexCount = 0;
            int triangleCount = 0;

            for (int i = 0; i < model.meshCount; i++)
            {
                vertexCount += model.meshes[i].vertexCount;
                triangleCount += model.meshes[i].triangleCount;
            }

            DrawText("Main model", (int)x0, (int)yPos, 10, LIME);
            DrawText(TextFormat("%d", vertexCount), (int)x1, (int)yPos, 10, LIME);
            DrawText(TextFormat("%d", triangleCount), (int)x2, (int)yPos, 10, LIME);
            yPos += 15;

            for (int i = 0; i < decalCount; i++)
            {
                if (i == 20)
                {
                    DrawText("...", (int)x0, (int)yPos, 10, LIME);
                    yPos += 15;
                }

                if (i < 20)
                {
                    DrawText(TextFormat("Decal #%d", i+1), (int)x0, (int)yPos, 10, LIME);
                    DrawText(TextFormat("%d", decalModels[i].meshes[0].vertexCount), (int)x1, (int)yPos, 10, LIME);
                    DrawText(TextFormat("%d", decalModels[i].meshes[0].triangleCount), (int)x2, (int)yPos, 10, LIME);
                    yPos += 15;
                }

                vertexCount += decalModels[i].meshes[0].vertexCount;
                triangleCount += decalModels[i].meshes[0].triangleCount;
            }

            DrawText("TOTAL", (int)x0, (int)yPos, 10, LIME);
            DrawText(TextFormat("%d", vertexCount), (int)x1, (int)yPos, 10, LIME);
            DrawText(TextFormat("%d", triangleCount), (int)x2, (int)yPos, 10, LIME);
            yPos += 15;

            DrawText("Hold RMB to move camera", 10, 430, 10, GRAY);
            DrawText("(c) Character model and texture from kenney.nl", screenWidth - 260, screenHeight - 20, 10, GRAY);

            // UI elements
            if (GuiButton((Rectangle){ 10, screenHeight - 100.0f, 100, 60 }, showModel ? "Hide Model" : "Show Model")) showModel = !showModel;

            if (GuiButton((Rectangle){ 10 + 110, screenHeight - 100.0f, 100, 60 }, "Clear Decals"))
            {
                // Clear decals, unload all decal models
                for (int i = 0; i < decalCount; i++) UnloadModel(decalModels[i]);
                decalCount = 0;
            }

            DrawFPS(10, 10);

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

    // De-Initialization
    //--------------------------------------------------------------------------------------
    UnloadModel(model);
    UnloadModel(placementCube);
    UnloadTexture(modelTexture);

    // Unload decal models
    for (int i = 0; i < decalCount; i++) UnloadModel(decalModels[i]);

    UnloadTexture(decalTexture);

    FreeDecalMeshData();        // Free the data for decal generation

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

    return 0;
}

//----------------------------------------------------------------------------------
// Module Functions Definition
//----------------------------------------------------------------------------------
// Add triangles to mesh builder (dynamic array manager)
static void AddTriangleToMeshBuilder(MeshBuilder *mb, Vector3 vertices[3])
{
    // Reallocate and copy if we need to
    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;
    }

    // Add 3 vertices
    int index = mb->vertexCount;
    mb->vertexCount += 3;

    for (int i = 0; i < 3; i++) mb->vertices[index+i] = vertices[i];
}

// Free mesh builder
static void FreeMeshBuilder(MeshBuilder *mb)
{
    MemFree(mb->vertices);
    if (mb->uvs != NULL) MemFree(mb->uvs);
    *mb = (MeshBuilder){ 0 };
}

// Build a Mesh from MeshBuilder data
static Mesh BuildMesh(MeshBuilder *mb)
{
    Mesh outMesh = { 0 };

    outMesh.vertexCount = mb->vertexCount;
    outMesh.triangleCount = mb->vertexCount/3;
    outMesh.vertices = MemAlloc(outMesh.vertexCount*3*sizeof(float));
    if (mb->uvs != NULL) outMesh.texcoords = 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;
}

// Clip segment
static Vector3 ClipSegment(Vector3 v0, Vector3 v1, Vector3 p, float s)
{
    float d0 = Vector3DotProduct(v0, p) - s;
    float d1 = Vector3DotProduct(v1, p) - s;
    float s0 = d0/(d0 - d1);

    Vector3 position = Vector3Lerp(v0, v1, s0);

    return position;
}

// Generate mesh decals for provided model
static Mesh GenMeshDecal(Model target, Matrix projection, float decalSize, float decalOffset)
{
    // We're going to use these to build up our decal meshes
    // They'll resize automatically as we go, we'll free them at the end
    static MeshBuilder meshBuilders[2] = { 0 };

    // Ugly way of telling us to free the static MeshBuilder data
    if (target.meshCount == -1)
    {
        FreeMeshBuilder(&meshBuilders[0]);
        FreeMeshBuilder(&meshBuilders[1]);
        return (Mesh){ 0 };
    }

    // We're going to need the inverse matrix
    Matrix invProj = MatrixInvert(projection);

    // Reset the mesh builders
    meshBuilders[0].vertexCount = 0;
    meshBuilders[1].vertexCount = 0;

    // We'll be flip-flopping between the two mesh builders
    // Reading from one and writing to the other, then swapping
    int mbIndex = 0;

    // First pass, just get any triangle inside the bounding box (for each mesh of the model)
    for (int meshIndex = 0; meshIndex < target.meshCount; meshIndex++)
    {
        Mesh mesh = target.meshes[meshIndex];
        for (int tri = 0; tri < mesh.triangleCount; tri++)
        {
            Vector3 vertices[3] = { 0 };

            // The way we calculate the vertices of the mesh triangle
            // depend on whether the mesh vertices are indexed or not
            if (mesh.indices == 0)
            {
                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++)
                {
                    vertices[v] = (Vector3){
                        mesh.vertices[ 3*mesh.indices[3*tri+0] + v],
                        mesh.vertices[ 3*mesh.indices[3*tri+1] + v],
                        mesh.vertices[ 3*mesh.indices[3*tri+2] + v]
                    };
                }
            }

            // Transform all 3 vertices of the triangle
            // and check if they are inside our decal box
            int insideCount = 0;
            for (int i = 0; i < 3; i++)
            {
                // To projection space
                Vector3 v = Vector3Transform(vertices[i], projection);

                if ((fabsf(v.x) < decalSize) || (fabsf(v.y) <= decalSize) || (fabsf(v.z) <= decalSize)) insideCount++;

                // We need to keep the transformed vertex
                vertices[i] = v;
            }

            // If any of them are inside, we add the triangle - we'll clip it later
            if (insideCount > 0) AddTriangleToMeshBuilder(&meshBuilders[mbIndex], vertices);
        }
    }

    // Clipping time! We need to clip against all 6 directions
    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++)
    {
        // Swap current model builder (so we read from the one we just wrote to)
        mbIndex = 1 - mbIndex;

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

        // Reset write builder
        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);

            // Calculate, how many vertices of the face lie outside of the clipping plane
            int total = v1Out + v2Out + v3Out;

            switch (total)
            {
                case 0:
                {
                    // The entire face lies inside of the plane, no clipping needed
                    AddTriangleToMeshBuilder(outMesh, (Vector3[3]){inMesh->vertices[i], inMesh->vertices[i+1], inMesh->vertices[i+2]});
                } break;
                case 1:
                {
                    // One vertex lies outside of the plane, perform clipping
                    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, (Vector3[3]){nV3, nV2, nV1});
                        AddTriangleToMeshBuilder(outMesh, (Vector3[3]){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, (Vector3[3]){nV1, nV2, nV3});
                    AddTriangleToMeshBuilder(outMesh, (Vector3[3]){nV4, nV3, nV2});
                } break;
                case 2:
                {
                    // Two vertices lies outside of the plane, perform clipping
                    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, (Vector3[3]){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, (Vector3[3]){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, (Vector3[3]){nV1, nV2, nV3});
                    }
                } break;
                case 3: // The entire face lies outside of the plane, so let's discard the corresponding vertices
                default: break;
            }
        }
    }

    // Now we just need to re-transform the vertices
    MeshBuilder *theMesh = &meshBuilders[mbIndex];

    // Allocate room for UVs
    if (theMesh->vertexCount > 0)
    {
        theMesh->uvs = (Vector2 *)MemAlloc(sizeof(Vector2)*theMesh->vertexCount);

        for (int i = 0; i < theMesh->vertexCount; i++)
        {
            // Calculate the UVs based on the projected coords
            // They are clipped to (-decalSize .. decalSize) and we want them (0..1)
            theMesh->uvs[i].x = (theMesh->vertices[i].x/decalSize + 0.5f);
            theMesh->uvs[i].y = (theMesh->vertices[i].y/decalSize + 0.5f);

            // Tiny nudge in the normal direction so it renders properly over the mesh
            theMesh->vertices[i].z -= decalOffset;

            // From projection space to world space
            theMesh->vertices[i] = Vector3Transform(theMesh->vertices[i], invProj);
        }

        // Decal model data ready, create the mesh and return it
        return BuildMesh(theMesh);
    }
    else
    {
        // Return a blank mesh as there's nothing to add
        return (Mesh){ 0 };
    }
}

// Button UI element
static bool GuiButton(Rectangle rec, const char *label)
{
    Color bgColor = GRAY;
    bool pressed = false;

    if (CheckCollisionPointRec(GetMousePosition(), rec))
    {
        bgColor = LIGHTGRAY;
        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) pressed = true;
    }

    DrawRectangleRec(rec, bgColor);
    DrawRectangleLinesEx(rec, 2.0f, DARKGRAY);

    int fontSize = 10;
    int textWidth = MeasureText(label, fontSize);

    DrawText(label, (int)(rec.x + rec.width*0.5f - textWidth*0.5f), (int)(rec.y + rec.height*0.5f - fontSize*0.5f), fontSize, DARKGRAY);

    return pressed;
}

From raylib 6.2. The original on GitHub.