procedural decals
There is no browser build of this example yet.
Is it the same as C?
- Desktop:verified The Zig version renders a final frame that is byte-identical to the C example.
- Browser:not checked yet The browser comparison does not cover this example yet.
//! 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.