From f97f9945128ab2b6f88c36282934865e8f915a47 Mon Sep 17 00:00:00 2001 From: PanicTitan <66186142+PanicTitan@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:49:18 -0300 Subject: [PATCH] New Examples New examples showing a 3d portal effect, light bloom, texture decals that works on the web, drawing on textures --- .../other_stencil_buffer_portal_masking.c | 220 +++++++ .../shaders_multi_lighting_with_bloom.c | 314 ++++++++++ examples/textures/textures_decals.c | 578 ++++++++++++++++++ .../textures_dynamic_uv_mesh_painter.c | 504 +++++++++++++++ 4 files changed, 1616 insertions(+) create mode 100644 examples/others/other_stencil_buffer_portal_masking.c create mode 100644 examples/shaders/shaders_multi_lighting_with_bloom.c create mode 100644 examples/textures/textures_decals.c create mode 100644 examples/textures/textures_dynamic_uv_mesh_painter.c diff --git a/examples/others/other_stencil_buffer_portal_masking.c b/examples/others/other_stencil_buffer_portal_masking.c new file mode 100644 index 000000000..042dfbaf7 --- /dev/null +++ b/examples/others/other_stencil_buffer_portal_masking.c @@ -0,0 +1,220 @@ +/******************************************************************************************* +* +* raylib [other] example - 3d stencil buffer portal masking +* +* Example complexity rating: [★★★☆] 3/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) +* +********************************************************************************************/ + +#include "raylib.h" +#include "rlgl.h" +#include + +// Standard OpenGL defines for Stencil Buffer and Face Culling +#ifndef GL_STENCIL_TEST + #define GL_STENCIL_TEST 0x0B90 + #define GL_STENCIL_BUFFER_BIT 0x00000400 + #define GL_DEPTH_BUFFER_BIT 0x00000100 + #define GL_CULL_FACE 0x0B44 + #define GL_BACK 0x0405 + #define GL_ALWAYS 0x0207 + #define GL_EQUAL 0x0202 + #define GL_REPLACE 0x1E01 + #define GL_KEEP 0x1E00 + #define GL_FALSE 0 + #define GL_TRUE 1 +#endif + +// External OpenGL function declarations +#if defined(__cplusplus) +extern "C" { +#endif + RLAPI void glEnable(unsigned int cap); + RLAPI void glDisable(unsigned int cap); + RLAPI void glClear(unsigned int mask); + RLAPI void glStencilFunc(unsigned int func, int ref, unsigned int mask); + RLAPI void glStencilOp(unsigned int sfail, unsigned int dpfail, unsigned int dppass); + RLAPI void glColorMask(unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha); + RLAPI void glDepthMask(unsigned char flag); + RLAPI void glCullFace(unsigned int mode); +#if defined(__cplusplus) +} +#endif + +//------------------------------------------------------------------------------------ +// Module Functions Declaration +//------------------------------------------------------------------------------------ +static void DrawPortalMaskPlane(float width, float height); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 1280; + const int screenHeight = 720; + + InitWindow(screenWidth, screenHeight, "raylib [rlgl] example - 3d stencil buffer portal"); + + // Camera to navigate the 3D world + Camera3D camera = { 0 }; + camera.position = (Vector3){ 0.0f, 2.5f, 7.0f }; + camera.target = (Vector3){ 0.0f, 1.8f, 0.0f }; + camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + camera.fovy = 45.0f; + camera.projection = CAMERA_PERSPECTIVE; + + DisableCursor(); // Lock cursor for first-person free camera controls + SetTargetFPS(60); // Set target frame-rate + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&camera, CAMERA_FREE); + float time = (float)GetTime(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground((Color){ 15, 18, 26, 255 }); + + // 1. Render Real World (Outside Portal) + //------------------------------------------------------------------------------ + BeginMode3D(camera); + DrawGrid(20, 1.0f); + + // Real world side pillars + DrawCube((Vector3){ -3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, DARKGRAY); + DrawCubeWires((Vector3){ -3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, ORANGE); + + DrawCube((Vector3){ 3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, DARKGRAY); + DrawCubeWires((Vector3){ 3.5f, 2.0f, 0.0f }, 0.8f, 4.0f, 0.8f, ORANGE); + + // Golden archway front frame + DrawCubeWires((Vector3){ 0.0f, 2.0f, 0.0f }, 3.2f, 4.2f, 0.2f, GOLD); + DrawCube((Vector3){ 0.0f, 0.05f, 0.0f }, 3.4f, 0.1f, 0.6f, MAROON); + + // Solid back wall for the portal (visible when viewing from behind) + DrawCube((Vector3){ 0.0f, 2.0f, -0.05f }, 3.1f, 4.1f, 0.05f, DARKBLUE); + EndMode3D(); + + // Flush pending render batch before modifying OpenGL stencil state + rlDrawRenderBatchActive(); + + // 2. Stencil Mask Pass (Write Portal Doorway to Stencil Buffer) + //------------------------------------------------------------------------------ + glEnable(GL_STENCIL_TEST); + glClear(GL_STENCIL_BUFFER_BIT); // Reset stencil mask buffer to 0 + + // Enable backface culling so the back of the portal plane does not register + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + + // Stencil settings: write '1' everywhere mask geometry is drawn + glStencilFunc(GL_ALWAYS, 1, 0xFF); + glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); + + // Disable color and depth writes (mask geometry remains invisible) + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + + BeginMode3D(camera); + // Draw single-sided 2D quad mask plane facing +Z + DrawPortalMaskPlane(3.0f, 4.0f); + EndMode3D(); + + rlDrawRenderBatchActive(); + + // Restore culling state + glDisable(GL_CULL_FACE); + + // 3. Render Secret World (Only where Stencil == 1) + //------------------------------------------------------------------------------ + // Restore color and depth writing + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + + // Only render fragments where stencil buffer is equal to 1 + glStencilFunc(GL_EQUAL, 1, 0xFF); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + + // Clear depth buffer in stencil region so inner geometry renders properly + glClear(GL_DEPTH_BUFFER_BIT); + + BeginMode3D(camera); + // Secret world neon grid floor + DrawGrid(30, 0.8f); + + // Floating pulsing core sphere inside alternate dimension + Vector3 corePos = { 0.0f, 2.0f + sinf(time * 2.5f) * 0.4f, -4.0f }; + DrawSphere(corePos, 1.2f, PURPLE); + DrawSphereWires(corePos, 1.25f, 16, 16, MAGENTA); + + // Orbiting green cubes in alternate dimension + for (int i = 0; i < 4; i++) + { + float angle = time * 1.5f + i * (PI / 2.0f); + Vector3 pos = { + sinf(angle) * 2.5f, + 2.0f + cosf(time * 3.0f + i) * 0.5f, + -4.0f + cosf(angle) * 2.5f + }; + DrawCube(pos, 0.5f, 0.5f, 0.5f, LIME); + DrawCubeWires(pos, 0.52f, 0.52f, 0.52f, DARKGREEN); + } + EndMode3D(); + + rlDrawRenderBatchActive(); + + // 4. Cleanup & 2D HUD Overlay + //------------------------------------------------------------------------------ + glDisable(GL_STENCIL_TEST); + + // Overlay HUD + DrawRectangle(15, 15, 420, 85, Fade(BLACK, 0.75f)); + DrawRectangleLines(15, 15, 420, 85, GOLD); + DrawText("3D STENCIL PORTAL DEMO", 28, 25, 20, GOLD); + DrawText("Look through the golden arch into Dimension B", 28, 52, 14, RAYWHITE); + DrawText("Controls: Mouse to look | WASD / Keys to move", 28, 72, 12, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +//---------------------------------------------------------------------------------- +// Module Functions Definition +//---------------------------------------------------------------------------------- + +// Draw a single 1-sided 2D quad plane facing +Z used for stencil masking +static void DrawPortalMaskPlane(float width, float height) +{ + float hw = width / 2.0f; + + rlBegin(RL_QUADS); + rlNormal3f(0.0f, 0.0f, 1.0f); + // Counter-Clockwise (CCW) vertex order = Front Face + rlVertex3f(-hw, 0.0f, 0.0f); + rlVertex3f( hw, 0.0f, 0.0f); + rlVertex3f( hw, height, 0.0f); + rlVertex3f(-hw, height, 0.0f); + rlEnd(); +} \ No newline at end of file diff --git a/examples/shaders/shaders_multi_lighting_with_bloom.c b/examples/shaders/shaders_multi_lighting_with_bloom.c new file mode 100644 index 000000000..c6de4cc90 --- /dev/null +++ b/examples/shaders/shaders_multi_lighting_with_bloom.c @@ -0,0 +1,314 @@ +/******************************************************************************************* +* +* raylib [shaders] example - forward multi-lighting with bloom +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* NOTE: Forward multi-point lighting combined with specular highlights and post-processing +* tone-mapped bloom shader pass using offscreen render textures. +* +* Example contributed by PanicTitan and reviewed by Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" + +#include // Required for: NULL +#include // Required for: sinf(), cosf() + +#if defined(PLATFORM_WEB) + #include +#endif + +#if defined(PLATFORM_DESKTOP) + #define GLSL_VERSION "#version 330\n" +#else // PLATFORM_ANDROID, PLATFORM_WEB + #define GLSL_VERSION "#version 300 es\nprecision mediump float;\n" +#endif + +//-------------------------------------------------------------------------------------- +// Global Definitions +//-------------------------------------------------------------------------------------- +#define MAX_LIGHTS 8 + +//-------------------------------------------------------------------------------------- +// Embedded Shader Sources +//-------------------------------------------------------------------------------------- + +// Forward Vertex Shader +static const char *multiLightVS = GLSL_VERSION R"( +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; + +out vec3 fragPosition; +out vec3 fragNormal; +out vec2 fragTexCoord; + +uniform mat4 mvp; +uniform mat4 matModel; + +void main() { + fragPosition = vec3(matModel * vec4(vertexPosition, 1.0)); + fragNormal = normalize(vec3(matModel * vec4(vertexNormal, 0.0))); + fragTexCoord = vertexTexCoord; + gl_Position = mvp * vec4(vertexPosition, 1.0); +} +)"; + +// Forward Fragment Shader (Point Light Falloff & Blinn-Phong Specular) +static const char *multiLightFS = GLSL_VERSION R"( +in vec3 fragPosition; +in vec3 fragNormal; +in vec2 fragTexCoord; + +out vec4 finalColor; + +uniform sampler2D texture0; +uniform vec3 viewPos; +uniform vec3 lightPositions[8]; +uniform vec3 lightColors[8]; + +void main() { + vec4 texColor = texture(texture0, fragTexCoord); + vec3 norm = normalize(fragNormal); + vec3 viewDir = normalize(viewPos - fragPosition); + + // Dark ambient baseline + vec3 ambient = 0.05 * texColor.rgb; + vec3 totalDiffuse = vec3(0.0); + vec3 totalSpecular = vec3(0.0); + + for (int i = 0; i < 8; i++) { + vec3 lightDir = normalize(lightPositions[i] - fragPosition); + float dist = length(lightPositions[i] - fragPosition); + + // Attenuation calculation + float attenuation = 1.0 / (1.0 + 0.8 * dist + 0.4 * dist * dist); + + // Diffuse Shading + float diff = max(dot(norm, lightDir), 0.0); + totalDiffuse += diff * lightColors[i] * attenuation * 1.2; + + // Blinn-Phong Specular + vec3 halfwayDir = normalize(lightDir + viewDir); + float spec = pow(max(dot(norm, halfwayDir), 0.0), 32.0); + totalSpecular += spec * lightColors[i] * attenuation * 0.8; + } + + vec3 result = ambient + (totalDiffuse * texColor.rgb) + totalSpecular; + finalColor = vec4(result, texColor.a); +} +)"; + +// Tone-Mapped Kawase Bloom Fragment Shader +static const char *bloomFS = GLSL_VERSION R"( +in vec2 fragTexCoord; +out vec4 finalColor; + +uniform sampler2D texture0; + +void main() { + vec2 uv = fragTexCoord; + vec4 color = texture(texture0, uv); + + // High-pass threshold check (pixels brighter than 0.75) + vec4 bloom = vec4(0.0); + vec2 texel = vec2(1.0 / 1280.0, 1.0 / 720.0) * 2.5; + + for (int x = -2; x <= 2; x++) { + for (int y = -2; y <= 2; y++) { + vec4 smp = texture(texture0, uv + vec2(x, y) * texel); + float brightness = max(smp.r, max(smp.g, smp.b)); + if (brightness > 0.75) bloom += smp; + } + } + bloom /= 25.0; + + // Combine original with soft bloom glow + vec3 HDR = color.rgb + bloom.rgb * 1.2; + + // Reinhard Tone Mapping + vec3 LDR = HDR / (HDR + vec3(1.0)); + + finalColor = vec4(LDR, color.a); +} +)"; + +//-------------------------------------------------------------------------------------- +// Types and Structures Definition +//-------------------------------------------------------------------------------------- +typedef struct AppContext { + Camera camera; // 3D scene camera + RenderTexture2D fbo; // Offscreen render target + Shader lightShader; // Multi-light forward shader + Shader bloomShader; // Bloom post-processing shader + Model cubeModel; // Center cube mesh model + Model floorModel; // Ground plane mesh model + + int lightPosLoc; // Uniform location: light positions + int lightColLoc; // Uniform location: light colors + int viewPosLoc; // Uniform location: view position + + Vector3 lightPositions[MAX_LIGHTS]; // Array of point light positions + Vector3 lightColors[MAX_LIGHTS]; // Array of point light RGB colors +} AppContext; + +//-------------------------------------------------------------------------------------- +// Module Global Variables +//-------------------------------------------------------------------------------------- +static AppContext g_App = { 0 }; + +//-------------------------------------------------------------------------------------- +// Module Functions Declaration +//-------------------------------------------------------------------------------------- +void UpdateDrawFrame(void); // Main frame loop (web & desktop) + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 1280; + const int screenHeight = 720; + + SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - forward multi-lighting bloom"); + + // Camera setup + g_App.camera.position = (Vector3){ 0.0f, 5.0f, 9.0f }; + g_App.camera.target = (Vector3){ 0.0f, 0.5f, 0.0f }; + g_App.camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + g_App.camera.fovy = 45.0f; + g_App.camera.projection = CAMERA_PERSPECTIVE; + + // Load render target for multi-pass bloom + g_App.fbo = LoadRenderTexture(screenWidth, screenHeight); + + // Load custom shaders from memory strings + g_App.lightShader = LoadShaderFromMemory(multiLightVS, multiLightFS); + g_App.bloomShader = LoadShaderFromMemory(NULL, bloomFS); + + // Create models and attach custom lighting shader + g_App.cubeModel = LoadModelFromMesh(GenMeshCube(2.0f, 2.0f, 2.0f)); + g_App.floorModel = LoadModelFromMesh(GenMeshPlane(14.0f, 14.0f, 1, 1)); + g_App.cubeModel.materials[0].shader = g_App.lightShader; + g_App.floorModel.materials[0].shader = g_App.lightShader; + + // Fetch shader uniform locations + g_App.lightPosLoc = GetShaderLocation(g_App.lightShader, "lightPositions"); + g_App.lightColLoc = GetShaderLocation(g_App.lightShader, "lightColors"); + g_App.viewPosLoc = GetShaderLocation(g_App.lightShader, "viewPos"); + + // Initialize 8 vivid light colors + g_App.lightColors[0] = (Vector3){ 1.0f, 0.2f, 0.2f }; // Red + g_App.lightColors[1] = (Vector3){ 0.2f, 1.0f, 0.3f }; // Green + g_App.lightColors[2] = (Vector3){ 0.2f, 0.5f, 1.0f }; // Blue + g_App.lightColors[3] = (Vector3){ 1.0f, 0.8f, 0.1f }; // Yellow + g_App.lightColors[4] = (Vector3){ 1.0f, 0.1f, 0.8f }; // Magenta + g_App.lightColors[5] = (Vector3){ 0.1f, 1.0f, 1.0f }; // Cyan + g_App.lightColors[6] = (Vector3){ 1.0f, 0.4f, 0.1f }; // Orange + g_App.lightColors[7] = (Vector3){ 0.7f, 0.2f, 1.0f }; // Purple + + SetShaderValueV(g_App.lightShader, g_App.lightColLoc, g_App.lightColors, SHADER_UNIFORM_VEC3, MAX_LIGHTS); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop +#if defined(PLATFORM_WEB) + emscripten_set_main_loop(UpdateDrawFrame, 0, 1); +#else + while (!WindowShouldClose()) + { + UpdateDrawFrame(); + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(g_App.cubeModel); + UnloadModel(g_App.floorModel); + UnloadShader(g_App.lightShader); + UnloadShader(g_App.bloomShader); + UnloadRenderTexture(g_App.fbo); + CloseWindow(); + //-------------------------------------------------------------------------------------- +#endif + + return 0; +} + +//-------------------------------------------------------------------------------------- +// Frame Update and Render Logic +//-------------------------------------------------------------------------------------- +void UpdateDrawFrame(void) +{ + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&g_App.camera, CAMERA_ORBITAL); + float time = (float)GetTime(); + + // Orbital path calculation for point lights + for (int i = 0; i < MAX_LIGHTS; i++) + { + float angle = (i / (float)MAX_LIGHTS) * 2.0f * PI + time * 0.6f; + float radius = 4.2f + sinf(time * 1.2f + i) * 0.4f; + float height = 1.0f + sinf(time * 1.8f + i) * 0.6f; + g_App.lightPositions[i] = (Vector3){ sinf(angle) * radius, height, cosf(angle) * radius }; + } + + SetShaderValueV(g_App.lightShader, g_App.lightPosLoc, g_App.lightPositions, SHADER_UNIFORM_VEC3, MAX_LIGHTS); + SetShaderValue(g_App.lightShader, g_App.viewPosLoc, &g_App.camera.position, SHADER_UNIFORM_VEC3); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // PASS 1: RENDER SCENE TO OFFSCREEN FBO + BeginTextureMode(g_App.fbo); + + ClearBackground((Color){ 12, 12, 18, 255 }); + + BeginMode3D(g_App.camera); + + DrawModel(g_App.cubeModel, (Vector3){ 0.0f, 1.0f, 0.0f }, 1.0f, GRAY); + DrawModel(g_App.floorModel, (Vector3){ 0.0f, 0.0f, 0.0f }, 1.0f, DARKGRAY); + DrawGrid(10, 1.0f); + + // Draw glowing orb bulbs at light origins + for (int i = 0; i < MAX_LIGHTS; i++) + { + Color bulbColor = (Color){ + (unsigned char)(g_App.lightColors[i].x * 255), + (unsigned char)(g_App.lightColors[i].y * 255), + (unsigned char)(g_App.lightColors[i].z * 255), + 255 + }; + DrawSphere(g_App.lightPositions[i], 0.15f, bulbColor); + } + + EndMode3D(); + + EndTextureMode(); + + // PASS 2: PRESENT WITH TONE-MAPPED BLOOM + BeginDrawing(); + + ClearBackground(BLACK); + + Rectangle srcRec = (Rectangle){ 0, 0, (float)g_App.fbo.texture.width, (float)-g_App.fbo.texture.height }; + + BeginShaderMode(g_App.bloomShader); + DrawTextureRec(g_App.fbo.texture, srcRec, (Vector2){ 0, 0 }, WHITE); + EndShaderMode(); + + DrawText("BALANCED MULTI-LIGHT + REINHARD TONE MAPPED BLOOM", 20, 20, 20, GREEN); + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- +} \ No newline at end of file diff --git a/examples/textures/textures_decals.c b/examples/textures/textures_decals.c new file mode 100644 index 000000000..a81142578 --- /dev/null +++ b/examples/textures/textures_decals.c @@ -0,0 +1,578 @@ +/******************************************************************************************* +* +* raylib [textures] example - decals +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* NOTE: Decal projection onto 3D mesh surface using geometry clipping. +* +* Example contributed by PanicTitan and reviewed by Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" + +#include +#include +#include + +#if defined(PLATFORM_WEB) + #include +#endif + +//-------------------------------------------------------------------------------------- +// Defines and Macros +//-------------------------------------------------------------------------------------- +#undef FLT_MAX +#define FLT_MAX 340282346638528859811704183484516925440.0f +#define MAX_DECALS 256 + +//-------------------------------------------------------------------------------------- +// Types and Structures Definition +//-------------------------------------------------------------------------------------- +typedef struct MeshBuilder { + int vertexCount; + int vertexCapacity; + Vector3 *vertices; + Vector2 *uvs; +} MeshBuilder; + +typedef struct AppContext { + Camera camera; + Model model; + Texture2D modelTexture; + Model placementCube; + Material decalMaterial; + Texture2D decalTexture; + BoundingBox modelBBox; + Model decalModels[MAX_DECALS]; + int decalCount; + float decalSize; + float decalOffset; + bool showModel; +} AppContext; + +//-------------------------------------------------------------------------------------- +// Global Variables Definition +//-------------------------------------------------------------------------------------- +static AppContext g_App = { 0 }; + +//-------------------------------------------------------------------------------------- +// 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 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); +static bool GuiButton(Rectangle rec, const char *label); +void UpdateDrawFrame(void); + +//------------------------------------------------------------------------------------ +// 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"); + + // Camera setup + g_App.camera.position = (Vector3){ 0.0f, 2.5f, 5.0f }; + g_App.camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; + g_App.camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + g_App.camera.fovy = 45.0f; + g_App.camera.projection = CAMERA_PERSPECTIVE; + + // Procedural model generation + Mesh knotMesh = GenMeshTorus(0.8f, 1.8f, 32, 64); + g_App.model = LoadModelFromMesh(knotMesh); + + // Procedural texture generation + Image modelImg = GenImageChecked(512, 512, 32, 32, LIGHTGRAY, GRAY); + g_App.modelTexture = LoadTextureFromImage(modelImg); + UnloadImage(modelImg); + SetTextureFilter(g_App.modelTexture, TEXTURE_FILTER_BILINEAR); + g_App.model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = g_App.modelTexture; + + // Bounding box setup + g_App.modelBBox = GetMeshBoundingBox(g_App.model.meshes[0]); + g_App.camera.target = Vector3Lerp(g_App.modelBBox.min, g_App.modelBBox.max, 0.5f); + + float modelSize = fminf( + fminf(fabsf(g_App.modelBBox.max.x - g_App.modelBBox.min.x), fabsf(g_App.modelBBox.max.y - g_App.modelBBox.min.y)), + fabsf(g_App.modelBBox.max.z - g_App.modelBBox.min.z)); + + g_App.decalSize = modelSize * 0.35f; + g_App.decalOffset = 0.01f; + + // Placement helper cube + g_App.placementCube = LoadModelFromMesh(GenMeshCube(g_App.decalSize, g_App.decalSize, g_App.decalSize)); + g_App.placementCube.materials[0].maps[0].color = LIME; + + // Procedural decal texture generation + Image decalImg = GenImageColor(128, 128, BLANK); + ImageDrawCircle(&decalImg, 64, 64, 60, RED); + ImageDrawCircle(&decalImg, 64, 64, 45, WHITE); + ImageDrawCircle(&decalImg, 64, 64, 30, RED); + ImageDrawCircle(&decalImg, 64, 64, 15, YELLOW); + + g_App.decalTexture = LoadTextureFromImage(decalImg); + UnloadImage(decalImg); + SetTextureFilter(g_App.decalTexture, TEXTURE_FILTER_BILINEAR); + + g_App.decalMaterial = LoadMaterialDefault(); + g_App.decalMaterial.maps[MATERIAL_MAP_DIFFUSE].texture = g_App.decalTexture; + g_App.decalMaterial.maps[MATERIAL_MAP_DIFFUSE].color = RAYWHITE; + + g_App.showModel = true; + g_App.decalCount = 0; + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop +#if defined(PLATFORM_WEB) + emscripten_set_main_loop(UpdateDrawFrame, 0, 1); +#else + while (!WindowShouldClose()) + { + UpdateDrawFrame(); + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(g_App.model); + UnloadTexture(g_App.modelTexture); + UnloadModel(g_App.placementCube); + UnloadTexture(g_App.decalTexture); + + for (int i = 0; i < g_App.decalCount; i++) UnloadModel(g_App.decalModels[i]); + + FreeDecalMeshData(); + CloseWindow(); + //-------------------------------------------------------------------------------------- +#endif + + return 0; +} + +//------------------------------------------------------------------------------------ +// Frame Update and Render Loop +//------------------------------------------------------------------------------------ +void UpdateDrawFrame(void) +{ + const int screenWidth = GetScreenWidth(); + const int screenHeight = GetScreenHeight(); + + // Update + //---------------------------------------------------------------------------------- + if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) UpdateCamera(&g_App.camera, CAMERA_THIRD_PERSON); + + RayCollision collision = { 0 }; + collision.distance = FLT_MAX; + collision.hit = false; + + Ray ray = GetScreenToWorldRay(GetMousePosition(), g_App.camera); + RayCollision boxHitInfo = GetRayCollisionBox(ray, g_App.modelBBox); + + if (boxHitInfo.hit && (g_App.decalCount < MAX_DECALS)) + { + RayCollision meshHitInfo = { 0 }; + for (int m = 0; m < g_App.model.meshCount; m++) + { + meshHitInfo = GetRayCollisionMesh(ray, g_App.model.meshes[m], g_App.model.transform); + if (meshHitInfo.hit) + { + if (!collision.hit || (collision.distance > meshHitInfo.distance)) collision = meshHitInfo; + } + } + } + + if (collision.hit && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && (g_App.decalCount < MAX_DECALS)) + { + Vector3 origin = Vector3Add(collision.point, Vector3Scale(collision.normal, 1.0f)); + Matrix splat = MatrixLookAt(collision.point, origin, (Vector3){ 0.0f, 1.0f, 0.0f }); + splat = MatrixMultiply(splat, MatrixRotateZ(DEG2RAD * ((float)GetRandomValue(-180, 180)))); + + Mesh decalMesh = GenMeshDecal(g_App.model, splat, g_App.decalSize, g_App.decalOffset); + + if (decalMesh.vertexCount > 0) + { + int decalIndex = g_App.decalCount++; + g_App.decalModels[decalIndex] = LoadModelFromMesh(decalMesh); + g_App.decalModels[decalIndex].materials[0].maps[0] = g_App.decalMaterial.maps[0]; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode3D(g_App.camera); + if (g_App.showModel) DrawModel(g_App.model, (Vector3){ 0.0f, 0.0f, 0.0f }, 1.0f, WHITE); + + for (int i = 0; i < g_App.decalCount; i++) DrawModel(g_App.decalModels[i], (Vector3){ 0 }, 1.0f, WHITE); + + if (collision.hit) + { + Vector3 origin = Vector3Add(collision.point, Vector3Scale(collision.normal, 1.0f)); + Matrix splat = MatrixLookAt(collision.point, origin, (Vector3){ 0, 1, 0 }); + g_App.placementCube.transform = MatrixInvert(splat); + DrawModel(g_App.placementCube, (Vector3){ 0 }, 1.0f, Fade(WHITE, 0.5f)); + } + + DrawGrid(10, 1.0f); + EndMode3D(); + + // Stats UI Display + float yPos = 10; + float x0 = screenWidth - 280.0f; + float x1 = x0 + 90; + float x2 = x1 + 90; + + 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 < g_App.model.meshCount; i++) + { + vertexCount += g_App.model.meshes[i].vertexCount; + triangleCount += g_App.model.meshes[i].triangleCount; + } + + DrawText("Base 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 < g_App.decalCount; i++) + { + vertexCount += g_App.decalModels[i].meshes[0].vertexCount; + triangleCount += g_App.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); + + DrawText("Left Click: Place Decal | Hold Right Click: Rotate Camera", 10, screenHeight - 25, 10, DARKGRAY); + + // GUI controls + if (GuiButton((Rectangle){ 10, (float)screenHeight - 80.0f, 100, 40 }, g_App.showModel ? "Hide Model" : "Show Model")) + { + g_App.showModel = !g_App.showModel; + } + + if (GuiButton((Rectangle){ 120, (float)screenHeight - 80.0f, 100, 40 }, "Clear Decals")) + { + for (int i = 0; i < g_App.decalCount; i++) UnloadModel(g_App.decalModels[i]); + g_App.decalCount = 0; + } + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- +} + +//-------------------------------------------------------------------------------------- +// Module Functions Definitions +//-------------------------------------------------------------------------------------- + +// Add a triangle definition to mesh builder buffer +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; +} + +// Free mesh builder memory buffer +static void FreeMeshBuilder(MeshBuilder *mb) +{ + if (mb->vertices) MemFree(mb->vertices); + if (mb->uvs) MemFree(mb->uvs); + *mb = (MeshBuilder){ 0 }; +} + +// Build standard raylib Mesh from builder object +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) 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) + { + outMesh.texcoords[2 * i + 0] = mb->uvs[i].x; + outMesh.texcoords[2 * i + 1] = mb->uvs[i].y; + } + } + + UploadMesh(&outMesh, false); + return outMesh; +} + +// Helper to clean up static internal mesh builder allocations +static void FreeDecalMeshData(void) +{ + Model dummy = { 0 }; + dummy.meshCount = -1; + Matrix m = { 0 }; + GenMeshDecal(dummy, m, 0.0f, 0.0f); +} + +// Clip line segment against clipping plane +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); + + return Vector3Lerp(v0, v1, s0); +} + +// Generate a clipped decal mesh mapped over target model using projection matrix +static Mesh GenMeshDecal(Model target, Matrix projection, float decalSize, float decalOffset) +{ + static MeshBuilder meshBuilders[2] = { 0 }; + + if (target.meshCount == -1) + { + FreeMeshBuilder(&meshBuilders[0]); + FreeMeshBuilder(&meshBuilders[1]); + return (Mesh){ 0 }; + } + + Matrix invProj = MatrixInvert(projection); + + meshBuilders[0].vertexCount = 0; + meshBuilders[1].vertexCount = 0; + + int mbIndex = 0; + + 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 }; + + 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]); + } + } + + 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: + AddTriangleToMeshBuilder(outMesh, inMesh->vertices[i], inMesh->vertices[i + 1], inMesh->vertices[i + 2]); + break; + case 1: + 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: + 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: + break; + } + } + } + + MeshBuilder *theMesh = &meshBuilders[mbIndex]; + + if (theMesh->vertexCount > 0) + { + theMesh->uvs = (Vector2 *)MemAlloc(sizeof(Vector2) * theMesh->vertexCount); + + for (int i = 0; i < theMesh->vertexCount; i++) + { + theMesh->uvs[i].x = (theMesh->vertices[i].x / decalSize + 0.5f); + theMesh->uvs[i].y = (theMesh->vertices[i].y / decalSize + 0.5f); + + theMesh->vertices[i].z -= decalOffset; + theMesh->vertices[i] = Vector3Transform(theMesh->vertices[i], invProj); + } + + return BuildMesh(theMesh); + } + + return (Mesh){ 0 }; +} + +// Simple custom GUI button helper +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; +} \ No newline at end of file diff --git a/examples/textures/textures_dynamic_uv_mesh_painter.c b/examples/textures/textures_dynamic_uv_mesh_painter.c new file mode 100644 index 000000000..dddbbda1e --- /dev/null +++ b/examples/textures/textures_dynamic_uv_mesh_painter.c @@ -0,0 +1,504 @@ +/******************************************************************************************* +* +* raylib [textures] example - 3D dynamic UV mesh painter +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* NOTE: Raycasts against 3D mesh geometry to interpolate UV coordinates using barycentric +* coordinates, then dynamically renders continuous paint strokes into an in-memory canvas +* texture mapped onto the model material. +* +* Example contributed by PanicTitan and reviewed by Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" + +#include // Required for: NULL +#include // Required for: memset() +#include // Required for: fabsf(), floorf() + +#if defined(PLATFORM_WEB) + #include +#endif + +//-------------------------------------------------------------------------------------- +// Global Definitions +//-------------------------------------------------------------------------------------- +#define CANVAS_SIZE 512 +#define MAX_PALETTE_COLORS 8 + +//-------------------------------------------------------------------------------------- +// Types and Structures Definition +//-------------------------------------------------------------------------------------- +typedef enum ToolMode { + TOOL_PAINT = 0, + TOOL_COLOR_PICKER +} ToolMode; + +typedef enum ShapeType { + SHAPE_SPHERE = 0, + SHAPE_CUBE, + SHAPE_CYLINDER, + SHAPE_TORUS +} ShapeType; + +typedef struct AppContext { + Camera camera; // 3D camera positioning + Model model; // Current active 3D model + Image canvasImage; // CPU RAM pixel buffer + Texture2D canvasTexture; // GPU VRAM texture bound to model material + BoundingBox modelBBox; // Transformed bounding box for fast ray testing + + // Painter tool state + ToolMode currentTool; // Active tool mode + ShapeType currentShape; // Active mesh shape + Color activeColor; // Selected drawing color + int brushRadius; // Brush stroke radius (in pixels) + + // Stroke interpolation state + Vector2 lastHitUV; // Last valid UV hit coordinate + bool hasLastHit; // Stroke continuity flag + + // Swatch palette + Color palette[MAX_PALETTE_COLORS]; // Palette color array + int paletteCount; // Total color swatches +} AppContext; + +//-------------------------------------------------------------------------------------- +// Module Global Variables +//-------------------------------------------------------------------------------------- +static AppContext g_App = { 0 }; + +//-------------------------------------------------------------------------------------- +// Module Functions Declaration +//-------------------------------------------------------------------------------------- +static Vector3 CalculateBarycentric(Vector3 p, Vector3 a, Vector3 b, Vector3 c); +static bool GetMeshHitUV(Ray ray, Model model, Vector2 *outUV); +static void ChangeMeshShape(ShapeType newShape); +static bool DrawCustomButton(Rectangle rec, const char *label, bool active); + +void UpdateDrawFrame(void); // Main frame loop (web & desktop) + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 960; + const int screenHeight = 580; + + // SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT); + InitWindow(screenWidth, screenHeight, "raylib [models] example - 3d dynamic mesh painter"); + + // Camera setup (Zoomed out to comfortably frame 3D primitives) + g_App.camera.position = (Vector3){ 0.0f, 3.5f, 6.5f }; + g_App.camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; + g_App.camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + g_App.camera.fovy = 45.0f; + g_App.camera.projection = CAMERA_PERSPECTIVE; + + // Initialize CPU canvas buffer and dynamic GPU texture + g_App.canvasImage = GenImageColor(CANVAS_SIZE, CANVAS_SIZE, RAYWHITE); + ImageDrawRectangleLines(&g_App.canvasImage, 0, 0, CANVAS_SIZE, CANVAS_SIZE, LIGHTGRAY); + + g_App.canvasTexture = LoadTextureFromImage(g_App.canvasImage); + SetTextureFilter(g_App.canvasTexture, TEXTURE_FILTER_BILINEAR); + + // Generate starting 3D primitive mesh + ChangeMeshShape(SHAPE_SPHERE); + + // Default painter configuration + g_App.currentTool = TOOL_PAINT; + g_App.activeColor = RED; + g_App.brushRadius = 12; + g_App.hasLastHit = false; + + // Palette swatches initialization + g_App.palette[0] = RED; + g_App.palette[1] = ORANGE; + g_App.palette[2] = GOLD; + g_App.palette[3] = LIME; + g_App.palette[4] = SKYBLUE; + g_App.palette[5] = PURPLE; + g_App.palette[6] = DARKGRAY; + g_App.palette[7] = WHITE; + g_App.paletteCount = MAX_PALETTE_COLORS; + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop +#if defined(PLATFORM_WEB) + emscripten_set_main_loop(UpdateDrawFrame, 0, 1); +#else + while (!WindowShouldClose()) + { + UpdateDrawFrame(); + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(g_App.model); + UnloadImage(g_App.canvasImage); + UnloadTexture(g_App.canvasTexture); + CloseWindow(); + //-------------------------------------------------------------------------------------- +#endif + + return 0; +} + +//-------------------------------------------------------------------------------------- +// Frame Update and Render Logic +//-------------------------------------------------------------------------------------- +void UpdateDrawFrame(void) +{ + // Update + //---------------------------------------------------------------------------------- + const int screenWidth = GetScreenWidth(); + const int screenHeight = GetScreenHeight(); + Vector2 mousePos = GetMousePosition(); + + // UI Panel collision boundary check + Rectangle uiPanelRec = (Rectangle){ 10.0f, 10.0f, 230.0f, 490.0f }; + bool isMouseOverUI = CheckCollisionPointRec(mousePos, uiPanelRec); + + // Orbit camera controls (Right Mouse Button Drag) + if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) + { + UpdateCamera(&g_App.camera, CAMERA_THIRD_PERSON); + } + + // 3D Painting & Color Picker Processing + if (!isMouseOverUI && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) + { + Ray ray = GetScreenToWorldRay(mousePos, g_App.camera); + Vector2 hitUV = { 0 }; + + if (GetMeshHitUV(ray, g_App.model, &hitUV)) + { + int imgW = g_App.canvasImage.width; + int imgH = g_App.canvasImage.height; + + int px = (int)(hitUV.x * (float)imgW); + int py = (int)(hitUV.y * (float)imgH); + + if (g_App.currentTool == TOOL_PAINT) + { + float du = fabsf(hitUV.x - g_App.lastHitUV.x); + float dv = fabsf(hitUV.y - g_App.lastHitUV.y); + + // Seam jump guard: prevent continuous stroke rendering across UV boundaries + if (g_App.hasLastHit && (du < 0.25f) && (dv < 0.25f)) + { + int lastPx = (int)(g_App.lastHitUV.x * (float)imgW); + int lastPy = (int)(g_App.lastHitUV.y * (float)imgH); + + float dist = Vector2Distance((Vector2){ (float)lastPx, (float)lastPy }, (Vector2){ (float)px, (float)py }); + int steps = (int)(dist / 2.0f) + 1; + + for (int i = 0; i <= steps; i++) + { + float t = (float)i / (float)steps; + int cx = (int)Lerp((float)lastPx, (float)px, t); + int cy = (int)Lerp((float)lastPy, (float)py, t); + + ImageDrawCircle(&g_App.canvasImage, cx, cy, g_App.brushRadius, g_App.activeColor); + } + } + else + { + ImageDrawCircle(&g_App.canvasImage, px, py, g_App.brushRadius, g_App.activeColor); + } + + UpdateTexture(g_App.canvasTexture, g_App.canvasImage.data); + + g_App.lastHitUV = hitUV; + g_App.hasLastHit = true; + } + else if (g_App.currentTool == TOOL_COLOR_PICKER) + { + g_App.activeColor = GetImageColor(g_App.canvasImage, px, py); + g_App.currentTool = TOOL_PAINT; + g_App.hasLastHit = false; + } + } + else + { + g_App.hasLastHit = false; + } + } + else + { + g_App.hasLastHit = false; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground((Color){ 30, 32, 40, 255 }); + + // 3D Scene Rendering + BeginMode3D(g_App.camera); + + DrawModel(g_App.model, (Vector3){ 0.0f, 0.0f, 0.0f }, 1.0f, WHITE); + DrawGrid(10, 1.0f); + + EndMode3D(); + + // 2D Controls & UI Panel + DrawRectangleRec(uiPanelRec, Fade(BLACK, 0.8f)); + DrawRectangleLinesEx(uiPanelRec, 2.0f, DARKGRAY); + + DrawText("3D MESH PAINTER", 25, 22, 18, GOLD); + + // Tool Mode Toggles + if (DrawCustomButton((Rectangle){ 25, 55, 95, 32 }, "PAINT", g_App.currentTool == TOOL_PAINT)) + { + g_App.currentTool = TOOL_PAINT; + } + + if (DrawCustomButton((Rectangle){ 125, 55, 95, 32 }, "PICKER", g_App.currentTool == TOOL_COLOR_PICKER)) + { + g_App.currentTool = TOOL_COLOR_PICKER; + } + + // Primitive Shape Selector + DrawText("Mesh Shape:", 25, 100, 12, LIGHTGRAY); + if (DrawCustomButton((Rectangle){ 25, 120, 95, 28 }, "SPHERE", g_App.currentShape == SHAPE_SPHERE)) + { + ChangeMeshShape(SHAPE_SPHERE); + } + if (DrawCustomButton((Rectangle){ 125, 120, 95, 28 }, "CUBE", g_App.currentShape == SHAPE_CUBE)) + { + ChangeMeshShape(SHAPE_CUBE); + } + if (DrawCustomButton((Rectangle){ 25, 153, 95, 28 }, "CYLINDER", g_App.currentShape == SHAPE_CYLINDER)) + { + ChangeMeshShape(SHAPE_CYLINDER); + } + if (DrawCustomButton((Rectangle){ 125, 153, 95, 28 }, "TORUS", g_App.currentShape == SHAPE_TORUS)) + { + ChangeMeshShape(SHAPE_TORUS); + } + + // Active Color Display + DrawText("Active Color:", 25, 195, 12, LIGHTGRAY); + DrawRectangle(125, 193, 95, 20, g_App.activeColor); + DrawRectangleLines(125, 193, 95, 20, WHITE); + + // Color Palette Grid + DrawText("Palette Swatches:", 25, 225, 12, LIGHTGRAY); + for (int i = 0; i < g_App.paletteCount; i++) + { + int sx = 25 + (i % 4) * 48; + int sy = 245 + (i / 4) * 45; + Rectangle swatchRec = (Rectangle){ (float)sx, (float)sy, 40.0f, 38.0f }; + + DrawRectangleRec(swatchRec, g_App.palette[i]); + DrawRectangleLinesEx(swatchRec, 1.0f, WHITE); + + if (CheckCollisionPointRec(mousePos, swatchRec) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + { + g_App.activeColor = g_App.palette[i]; + } + } + + // Brush Size Adjustments + DrawText(TextFormat("Brush Size: %dpx", g_App.brushRadius), 25, 345, 12, LIGHTGRAY); + if (DrawCustomButton((Rectangle){ 25, 365, 95, 30 }, "SIZE -", false)) + { + if (g_App.brushRadius > 2) g_App.brushRadius -= 2; + } + if (DrawCustomButton((Rectangle){ 125, 365, 95, 30 }, "SIZE +", false)) + { + if (g_App.brushRadius < 64) g_App.brushRadius += 2; + } + + // Canvas Actions + if (DrawCustomButton((Rectangle){ 25, 410, 95, 30 }, "CLEAR", false)) + { + ImageClearBackground(&g_App.canvasImage, RAYWHITE); + ImageDrawRectangleLines(&g_App.canvasImage, 0, 0, CANVAS_SIZE, CANVAS_SIZE, LIGHTGRAY); + UpdateTexture(g_App.canvasTexture, g_App.canvasImage.data); + } + + if (DrawCustomButton((Rectangle){ 125, 410, 95, 30 }, "FILL", false)) + { + ImageClearBackground(&g_App.canvasImage, g_App.activeColor); + UpdateTexture(g_App.canvasTexture, g_App.canvasImage.data); + } + + // Instructions Footer & Overlay Info + DrawText("Left Click: Paint / Pick Color | Right Click Drag: Orbit Camera", 260, screenHeight - 25, 12, RAYWHITE); + DrawFPS(screenWidth - 90, 15); + + EndDrawing(); + //---------------------------------------------------------------------------------- +} + +//-------------------------------------------------------------------------------------- +// Module Functions Definition +//-------------------------------------------------------------------------------------- + +// Dynamic Primitive Mesh Generator +static void ChangeMeshShape(ShapeType newShape) +{ + if (g_App.model.meshCount > 0) + { + UnloadModel(g_App.model); + } + + Mesh mesh = { 0 }; + + switch (newShape) + { + case SHAPE_SPHERE: mesh = GenMeshSphere(1.5f, 48, 48); break; + case SHAPE_CUBE: mesh = GenMeshCube(2.2f, 2.2f, 2.2f); break; + case SHAPE_CYLINDER: mesh = GenMeshCylinder(1.2f, 2.5f, 36); break; + case SHAPE_TORUS: mesh = GenMeshTorus(0.6f, 1.6f, 32, 48); break; + default: mesh = GenMeshSphere(1.5f, 48, 48); break; + } + + g_App.model = LoadModelFromMesh(mesh); + + // GenMeshCylinder builds upwards from Y=0 to Y=height (2.5). Translate down by height/2 to center at origin. + if (newShape == SHAPE_CYLINDER) + { + g_App.model.transform = MatrixTranslate(0.0f, -1.25f, 0.0f); + } + + g_App.model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = g_App.canvasTexture; + + // Compute bounding box and apply model transformation + BoundingBox bbox = GetMeshBoundingBox(g_App.model.meshes[0]); + bbox.min = Vector3Transform(bbox.min, g_App.model.transform); + bbox.max = Vector3Transform(bbox.max, g_App.model.transform); + g_App.modelBBox = bbox; + + g_App.currentShape = newShape; + g_App.hasLastHit = false; +} + +// Computes Barycentric Weights for UV Interpolation inside a 3D Triangle +static Vector3 CalculateBarycentric(Vector3 p, Vector3 a, Vector3 b, Vector3 c) +{ + Vector3 v0 = Vector3Subtract(b, a); + Vector3 v1 = Vector3Subtract(c, a); + Vector3 v2 = Vector3Subtract(p, a); + + float d00 = Vector3DotProduct(v0, v0); + float d01 = Vector3DotProduct(v0, v1); + float d11 = Vector3DotProduct(v1, v1); + float d20 = Vector3DotProduct(v2, v0); + float d21 = Vector3DotProduct(v2, v1); + + float denom = d00 * d11 - d01 * d01; + if (fabsf(denom) < 1e-6f) return (Vector3){ 0.33f, 0.33f, 0.34f }; + + float v = (d11 * d20 - d01 * d21) / denom; + float w = (d00 * d21 - d01 * d20) / denom; + float u = 1.0f - v - w; + + return (Vector3){ u, v, w }; +} + +// Raycasts against the model mesh and calculates exact UV texture space coordinates +static bool GetMeshHitUV(Ray ray, Model model, Vector2 *outUV) +{ + if (model.meshCount == 0 || model.meshes[0].texcoords == NULL) return false; + + // Fast bounding box pre-pass using transformed bounding box + RayCollision boxHit = GetRayCollisionBox(ray, g_App.modelBBox); + if (!boxHit.hit) return false; + + Mesh mesh = model.meshes[0]; + float closestDistance = 999999.0f; + bool foundHit = false; + Vector2 interpolatedUV = { 0 }; + + for (int tri = 0; tri < mesh.triangleCount; tri++) + { + int idx0, idx1, idx2; + + if (mesh.indices != NULL) + { + idx0 = mesh.indices[3 * tri + 0]; + idx1 = mesh.indices[3 * tri + 1]; + idx2 = mesh.indices[3 * tri + 2]; + } + else + { + idx0 = 3 * tri + 0; + idx1 = 3 * tri + 1; + idx2 = 3 * tri + 2; + } + + Vector3 a = Vector3Transform((Vector3){ mesh.vertices[3 * idx0 + 0], mesh.vertices[3 * idx0 + 1], mesh.vertices[3 * idx0 + 2] }, model.transform); + Vector3 b = Vector3Transform((Vector3){ mesh.vertices[3 * idx1 + 0], mesh.vertices[3 * idx1 + 1], mesh.vertices[3 * idx1 + 2] }, model.transform); + Vector3 c = Vector3Transform((Vector3){ mesh.vertices[3 * idx2 + 0], mesh.vertices[3 * idx2 + 1], mesh.vertices[3 * idx2 + 2] }, model.transform); + + // Backface culling check: ignore triangles oriented away from camera ray + Vector3 edge1 = Vector3Subtract(b, a); + Vector3 edge2 = Vector3Subtract(c, a); + Vector3 normal = Vector3Normalize(Vector3CrossProduct(edge1, edge2)); + + if (Vector3DotProduct(normal, ray.direction) >= 0.0f) continue; + + RayCollision triHit = GetRayCollisionTriangle(ray, a, b, c); + + if (triHit.hit && triHit.distance < closestDistance) + { + closestDistance = triHit.distance; + foundHit = true; + + Vector2 uvA = (Vector2){ mesh.texcoords[2 * idx0 + 0], mesh.texcoords[2 * idx0 + 1] }; + Vector2 uvB = (Vector2){ mesh.texcoords[2 * idx1 + 0], mesh.texcoords[2 * idx1 + 1] }; + Vector2 uvC = (Vector2){ mesh.texcoords[2 * idx2 + 0], mesh.texcoords[2 * idx2 + 1] }; + + Vector3 weights = CalculateBarycentric(triHit.point, a, b, c); + + interpolatedUV.x = weights.x * uvA.x + weights.y * uvB.x + weights.z * uvC.x; + interpolatedUV.y = weights.x * uvA.y + weights.y * uvB.y + weights.z * uvC.y; + + // Wrap UV values within [0.0..1.0] range + interpolatedUV.x = interpolatedUV.x - floorf(interpolatedUV.x); + interpolatedUV.y = interpolatedUV.y - floorf(interpolatedUV.y); + } + } + + if (foundHit && (outUV != NULL)) + { + *outUV = interpolatedUV; + } + + return foundHit; +} + +// Custom GUI Button Control +static bool DrawCustomButton(Rectangle rec, const char *label, bool active) +{ + Vector2 mousePos = GetMousePosition(); + bool hovered = CheckCollisionPointRec(mousePos, rec); + bool clicked = hovered && IsMouseButtonPressed(MOUSE_BUTTON_LEFT); + + Color btnColor = active ? GOLD : (hovered ? LIGHTGRAY : GRAY); + Color textColor = active ? BLACK : (hovered ? BLACK : RAYWHITE); + + DrawRectangleRec(rec, btnColor); + DrawRectangleLinesEx(rec, 1.5f, 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, textColor); + + return clicked; +} \ No newline at end of file