packages feed

h-raylib-6.1.0.0: raylib/examples/models/models_procedural_decals.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
*
*   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;
}