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 - 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.