From 44226c995a6f33ed77410acc58d1ad7b5224876a Mon Sep 17 00:00:00 2001 From: iann Date: Fri, 31 Oct 2025 04:17:27 +0900 Subject: [PATCH] cleaner commit on review branch --- examples/Makefile | 1 + .../core/core_3d_fixed_function_didactic.c | 646 ++++++++++++++++++ .../core/core_3d_fixed_function_didactic.png | Bin 0 -> 58096 bytes src/rmodels.c | 13 +- 4 files changed, 652 insertions(+), 8 deletions(-) create mode 100644 examples/core/core_3d_fixed_function_didactic.c create mode 100644 examples/core/core_3d_fixed_function_didactic.png diff --git a/examples/Makefile b/examples/Makefile index b5e968079..239c6974c 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -521,6 +521,7 @@ CORE = \ core/core_3d_camera_mode \ core/core_3d_camera_split_screen \ core/core_3d_picking \ + core/core_3d_fixed_function_didactic \ core/core_automation_events \ core/core_basic_screen_manager \ core/core_basic_window \ diff --git a/examples/core/core_3d_fixed_function_didactic.c b/examples/core/core_3d_fixed_function_didactic.c new file mode 100644 index 000000000..213fd2c06 --- /dev/null +++ b/examples/core/core_3d_fixed_function_didactic.c @@ -0,0 +1,646 @@ +/******************************************************************************************* +* +* raylib [core] example - Fixed-function didactic +* +* RESOURCES: +* - https://en.wikipedia.org/wiki/Fixed-function_(computer_graphics) +* - https://en.wikipedia.org/wiki/Texture_mapping#Perspective_correctness +* - Etay Meiri (OGLDEV) Perspective Projection Tutorial: https://www.youtube.com/watch?v=LhQ85bPCAJ8 +* - Keenan Crane Computer Graphics (CMU 15-462/662): https://www.youtube.com/watch?v=_4Q4O2Kgdo4 +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 6.0? (target) +* +* Example contributed by IANN (@meisei4) and reviewed by Ramon Santamaria (@raysan5) and community +* +* 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-2025 @meisei4 +* +********************************************************************************************/ +// TODO list: +// 1. add proper clipping to the target meshes to show intuition there (e.g. move mesh out of clip planes or allow moving the main camera's target away from the meshes) +// 2. improve didactic annotations (ideally with spatial labeling rather than simple flat screen overlay) +// 3. improve code didactic, code should read in order of fixed function staging... difficult but long term goal... +// 4. add scripted toggling/navigation of ordered fixed function staging visualization (a "play button"-like thing) +// 5. add some sort of ghosting effect between fixed function stages, to emphasize previous stages perhaps) +// 6. general improvements to toggling and space navigation + +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" +#include +#include +#include +#include + +#define BAHAMA_BLUE CLITERAL(Color){ 0, 102, 153, 255 } +#define SUNFLOWER CLITERAL(Color){ 255, 204, 153, 255 } +#define PALE_CANARY CLITERAL(Color){ 255, 255, 153, 255 } +#define ANAKIWA CLITERAL(Color){ 153, 204, 255, 255 } +#define MARINER CLITERAL(Color){ 51, 102, 204, 255 } +#define NEON_CARROT CLITERAL(Color){ 255, 153, 51, 255 } +#define EGGPLANT CLITERAL(Color){ 102, 68, 102, 255 } +#define HOPBUSH CLITERAL(Color){ 204, 102, 153, 255 } +#define LILAC CLITERAL(Color){ 204, 153, 204, 255 } +#define RED_DAMASK CLITERAL(Color){ 221, 102, 68, 255 } +#define CHESTNUT_ROSE CLITERAL(Color){ 204, 102, 102, 255 } + +typedef unsigned short Triangle[3]; + +enum Flags +{ + FLAG_NDC = 1u<<0, + FLAG_REFLECT_Y = 1u<<1, + FLAG_ASPECT = 1u<<2, + FLAG_PERSPECTIVE_CORRECT = 1u<<3, + FLAG_PAUSE = 1u<<4, + FLAG_COLOR_MODE = 1u<<5, + FLAG_TEXTURE_MODE = 1u<<6 +}; + +static unsigned int gflags = FLAG_ASPECT | FLAG_COLOR_MODE; + +#define NDC_SPACE() ((gflags & FLAG_NDC) != 0) +#define REFLECT_Y() ((gflags & FLAG_REFLECT_Y) != 0) +#define ASPECT_CORRECT() ((gflags & FLAG_ASPECT) != 0) +#define PERSPECTIVE_CORRECT() ((gflags & FLAG_PERSPECTIVE_CORRECT) != 0) +#define PAUSED() ((gflags & FLAG_PAUSE) != 0) +#define COLOR_MODE() ((gflags & FLAG_COLOR_MODE) != 0) +#define TEXTURE_MODE() ((gflags & FLAG_TEXTURE_MODE) != 0) +#define TOGGLE(K, F) do { if (IsKeyPressed(K)) { gflags ^= (F); } } while (0) + +static int fontSize = 20; +static float angularVelocity = 1.25f; +static float fovy = 60.0f; +static float blendScalar = 5.0f; +static Vector3 yAxis = { 0.0f, 1.0f, 0.0f }; +static Vector3 modelPos = { 0.0f, 0.0f, 0.0f }; +static Vector3 modelScale = { 1.0f, 1.0f, 1.0f }; +static Vector3 mainPos = { 0.0f, 0.0f, 2.0f }; +static Vector3 jugemuPosIso = { 3.0f, 1.0f, 3.0f }; + +static void BasisVector(Camera3D *main, Vector3 *depthOut, Vector3 *rightOut, Vector3 *upOut); +static void WorldToNDCSpace(Camera3D *main, float aspect, float near, float far, Model *world, Model *ndc, float rotation); + +static void DrawModelFilled(Model *model, Texture2D texture, float rotation); +static void DrawModelWiresAndPoints(Model *model, float rotation); +static void DrawNearPlanePoints(Camera3D *main, float aspect, float near, Model *nearPlanePointsModel, Mesh *mesh, float rotation); + +static void UpdateSpatialFrame(Camera3D *main, float aspect, float near, float far, Mesh *spatialFrame); +static void DrawSpatialFrame(Mesh spatialFrame); + +static void PerspectiveIncorrectCapture(Camera3D *main, float aspect, float near, Mesh *mesh, Texture2D meshTexture, float rotation); +static void PerspectiveCorrectCapture(Camera3D *main, Model *model, Texture2D meshTexture, Texture2D *perspectiveCorrectTexture, float rotation); + +static void AlphaMaskPunchOut(Image *rgba, Image *mask, unsigned char threshold); +static void FillVertexColors(Mesh *mesh); +static void OrbitSpace(Camera3D *jugemu, float dt); +static Vector3 AspectCorrectAndReflectNearPlane(Vector3 intersect, Vector3 center, Vector3 right, Vector3 up, float xAspect, float yReflect); +static Vector3 TranslateRotateScale(int inverse, Vector3 coordinate, Vector3 pos, Vector3 scale, float rotation); +static Vector3 Intersect(Camera3D *main, float near, Vector3 worldCoord); +static float SpaceBlendFactor(float dt); +static float AspectBlendFactor(float dt); +static float ReflectBlendFactor(float dt); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [core] example - fixed function didactic"); + + float aspect = (float)GetScreenWidth()/(float)GetScreenHeight(); + float meshRotation = 0.0f; + + Camera3D main = { 0 }; + main.position = mainPos; + main.target = modelPos; + main.up = yAxis; + main.fovy = fovy; + main.projection = CAMERA_PERSPECTIVE; + + Camera3D jugemu = (Camera3D){ 0 }; + jugemu.position = jugemuPosIso; + jugemu.target = modelPos; + jugemu.up = yAxis; + jugemu.fovy = fovy; + jugemu.projection = CAMERA_PERSPECTIVE; + + // Model worldModel = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + // Image textureImage = GenImageChecked(4, 4, 1, 1, BLACK, WHITE); + + // Model worldModel = LoadModelFromMesh(GenMeshSphere(0.5f, 8, 8)); + // Image textureImage = GenImageChecked(16, 16, 1, 1, BLACK, WHITE); + + // Model worldModel = LoadModelFromMesh(GenMeshKnot(1.0f, 1.0f, 8, 64)); + Model worldModel = LoadModelFromMesh(GenMeshKnot(1.0f, 1.0f, 16, 128)); + Image textureImage = GenImageChecked(32, 32, 1, 1, BLACK, WHITE); + + Texture2D meshTexture = LoadTextureFromImage(textureImage); + UnloadImage(textureImage); + + if (!worldModel.meshes[0].indices) + { + worldModel.meshes[0].indices = RL_CALLOC(worldModel.meshes[0].vertexCount, sizeof(unsigned short)); + for (int i = 0; i < worldModel.meshes[0].vertexCount; i++) worldModel.meshes[0].indices[i] = (unsigned short)i; + worldModel.meshes[0].triangleCount = worldModel.meshes[0].vertexCount/3; + } + FillVertexColors(&worldModel.meshes[0]); + + Mesh ndcMesh = (Mesh){ 0 }; + ndcMesh.vertexCount = worldModel.meshes[0].vertexCount; + ndcMesh.triangleCount = worldModel.meshes[0].triangleCount; + ndcMesh.vertices = RL_CALLOC(ndcMesh.vertexCount, sizeof(Vector3)); + ndcMesh.texcoords = RL_CALLOC(ndcMesh.vertexCount, sizeof(Vector2)); + ndcMesh.indices = RL_CALLOC(ndcMesh.triangleCount, sizeof(Triangle)); + ndcMesh.colors = RL_CALLOC(ndcMesh.vertexCount, sizeof(Color)); + memcpy(ndcMesh.colors, worldModel.meshes[0].colors, ndcMesh.vertexCount*sizeof(Color)); + memcpy(ndcMesh.texcoords, worldModel.meshes[0].texcoords, ndcMesh.vertexCount*sizeof(Vector2)); + memcpy(ndcMesh.indices, worldModel.meshes[0].indices, ndcMesh.triangleCount*sizeof(Triangle)); + Model ndcModel = LoadModelFromMesh(ndcMesh); + + Mesh nearPlanePoints = (Mesh){ 0 }; + nearPlanePoints.vertexCount = worldModel.meshes[0].triangleCount*3; + nearPlanePoints.vertices = RL_CALLOC(nearPlanePoints.vertexCount, sizeof(Vector3)); + Model nearPlanePointsModel = LoadModelFromMesh(nearPlanePoints); + + worldModel.materials[0].maps[MATERIAL_MAP_ALBEDO].texture = meshTexture; + ndcModel.materials[0].maps[MATERIAL_MAP_ALBEDO].texture = meshTexture; + + Texture2D perspectiveCorrectTexture = (Texture2D){ 0 }; + Mesh spatialFrame = GenMeshCube(1.0f, 1.0f, 1.0f); + spatialFrame.colors = RL_CALLOC(spatialFrame.vertexCount, sizeof(Color)); + for (int i = 0; i < spatialFrame.vertexCount; i++) ((Color *)spatialFrame.colors)[i] = (Color){ 255, 255, 255, 0 }; + for (int i = 0; i < 4; i++) ((Color *)spatialFrame.colors)[i].a = 255; + Model spatialFrameModel = LoadModelFromMesh(spatialFrame); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + float far = 3.0f; + float near = 1.0f; + aspect = (float)GetScreenWidth()/(float)GetScreenHeight(); + TOGGLE(KEY_N, FLAG_NDC); + if (NDC_SPACE()) TOGGLE(KEY_F, FLAG_REFLECT_Y); + TOGGLE(KEY_Q, FLAG_ASPECT); + TOGGLE(KEY_P, FLAG_PERSPECTIVE_CORRECT); + TOGGLE(KEY_SPACE, FLAG_PAUSE); + TOGGLE(KEY_C, FLAG_COLOR_MODE); + TOGGLE(KEY_T, FLAG_TEXTURE_MODE); + + float sBlend = SpaceBlendFactor(GetFrameTime()); + AspectBlendFactor(GetFrameTime()); + ReflectBlendFactor(GetFrameTime()); + + if (!PAUSED()) meshRotation -= angularVelocity*GetFrameTime(); + + OrbitSpace(&jugemu, GetFrameTime()); + + WorldToNDCSpace(&main, aspect, near, far, &worldModel, &ndcModel, meshRotation); + + for (int i = 0; i < ndcModel.meshes[0].vertexCount; i++) + { + Vector3 *worldVertices = (Vector3 *)worldModel.meshes[0].vertices; + Vector3 *ndcVertices = (Vector3 *)ndcModel.meshes[0].vertices; + ndcVertices[i].x = Lerp(worldVertices[i].x, ndcVertices[i].x, sBlend); + ndcVertices[i].y = Lerp(worldVertices[i].y, ndcVertices[i].y, sBlend); + ndcVertices[i].z = Lerp(worldVertices[i].z, ndcVertices[i].z, sBlend); + } + + Model *displayModel = &ndcModel; + Mesh *displayMesh = &ndcModel.meshes[0]; + + if (PERSPECTIVE_CORRECT() && TEXTURE_MODE()) + { + PerspectiveCorrectCapture(&main, displayModel, meshTexture, &perspectiveCorrectTexture, meshRotation); + } + + UpdateSpatialFrame(&main, aspect, near, far, &spatialFrame); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(BLACK); + + BeginMode3D(jugemu); + Vector3 depth, right, up; + BasisVector(&main, &depth, &right, &up); + + DrawLine3D(main.position, Vector3Add(main.position, right), NEON_CARROT); + DrawLine3D(main.position, Vector3Add(main.position, up), LILAC); + DrawLine3D(main.position, Vector3Add(main.position, depth), MARINER); + + DrawSpatialFrame(spatialFrame); + + DrawModelFilled(displayModel, meshTexture, meshRotation); + DrawModelWiresAndPoints(displayModel, meshRotation); + + DrawNearPlanePoints(&main, aspect, near, &nearPlanePointsModel, displayMesh, meshRotation); + + if (PERSPECTIVE_CORRECT() && TEXTURE_MODE()) + { + spatialFrameModel.materials[0].maps[MATERIAL_MAP_ALBEDO].texture = perspectiveCorrectTexture; + DrawModel(spatialFrameModel, modelPos, 1.0f, WHITE); + } + else + { + PerspectiveIncorrectCapture(&main, aspect, near, displayMesh, meshTexture, meshRotation); + } + EndMode3D(); + + DrawText("ARROWS: MOVE | SPACEBAR: PAUSE", 12, 12, fontSize, NEON_CARROT); + DrawText("W A : ZOOM", 12, 38, fontSize, NEON_CARROT); + DrawText("TEXTURE [ T ]:", 570, 12, fontSize, SUNFLOWER); + DrawText((TEXTURE_MODE())? "ON" : "OFF", 740, 12, fontSize, (TEXTURE_MODE())? ANAKIWA : CHESTNUT_ROSE); + DrawText("COLORS [ C ]:", 570, 38, fontSize, SUNFLOWER); + DrawText((COLOR_MODE())? "ON" : "OFF", 740, 38, fontSize, (COLOR_MODE())? ANAKIWA : CHESTNUT_ROSE); + DrawText("ASPECT [ Q ]:", 12, 392, fontSize, SUNFLOWER); + DrawText((ASPECT_CORRECT())? "CORRECT" : "INCORRECT", 230, 392, fontSize, (ASPECT_CORRECT())? ANAKIWA : CHESTNUT_ROSE); + DrawText("PERSPECTIVE [ P ]:", 12, 418, fontSize, SUNFLOWER); + DrawText((PERSPECTIVE_CORRECT())? "CORRECT" : "INCORRECT", 230, 418, fontSize, (PERSPECTIVE_CORRECT())? ANAKIWA : CHESTNUT_ROSE); + DrawText("SPACE [ N ]:", 530, 392, fontSize, SUNFLOWER); + DrawText((NDC_SPACE())? "NDC" : "WORLD", 665, 392, fontSize, (NDC_SPACE())? BAHAMA_BLUE : ANAKIWA); + if (NDC_SPACE()) + { + DrawText("REFLECT [ F ]:", 530, 418, fontSize, SUNFLOWER); + DrawText((REFLECT_Y())? "Y_DOWN" : "Y_UP", 695, 418, fontSize, (REFLECT_Y())? ANAKIWA : CHESTNUT_ROSE); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(worldModel); + UnloadModel(ndcModel); + UnloadModel(nearPlanePointsModel); + UnloadModel(spatialFrameModel); + UnloadTexture(meshTexture); + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +static void BasisVector(Camera3D *main, Vector3 *depthOut, Vector3 *rightOut, Vector3 *upOut) +{ + Vector3 depth = Vector3Normalize(Vector3Subtract(main->target, main->position)); + Vector3 right = Vector3Normalize(Vector3CrossProduct(depth, main->up)); + Vector3 up = Vector3Normalize(Vector3CrossProduct(right, depth)); + *depthOut = depth; + *rightOut = right; + *upOut = up; +} + +static void WorldToNDCSpace(Camera3D *main, float aspect, float near, float far, Model *world, Model *ndc, float rotation) +{ + Vector3 depth, right, up; + BasisVector(main, &depth, &right, &up); + float halfHNear = near*tanf(DEG2RAD*main->fovy*0.5f); + float halfWNear = Lerp(halfHNear, halfHNear*aspect, AspectBlendFactor(0.0f)); + float halfDepthNDC = Lerp(halfHNear, 0.5f*(far - near), AspectBlendFactor(0.0f)); + Vector3 centerNearPlane = Vector3Add(main->position, Vector3Scale(depth, near)); + Vector3 centerNDCCube = Vector3Add(centerNearPlane, Vector3Scale(depth, halfDepthNDC)); + + for (int i = 0; i < world->meshes[0].vertexCount; i++) + { + Vector3 worldVertex = TranslateRotateScale(0, ((Vector3 *)world->meshes[0].vertices)[i], modelPos, modelScale, rotation); + float signedDepth = Vector3DotProduct(Vector3Subtract(worldVertex, main->position), depth); + Vector3 intersectionCoord = Intersect(main, near, worldVertex); + Vector3 clipPlaneVector = Vector3Subtract(intersectionCoord, centerNearPlane); + float xNDC = Vector3DotProduct(clipPlaneVector, right)/halfWNear; + float yNDC = Vector3DotProduct(clipPlaneVector, up)/halfHNear; + float zNDC = (far + near - 2.0f*far*near/signedDepth)/(far - near); + Vector3 scaledRight = Vector3Scale(right, xNDC*halfWNear); + Vector3 scaledUp = Vector3Scale(up, yNDC*halfHNear); + Vector3 scaledDepth = Vector3Scale(depth, zNDC*halfDepthNDC); + Vector3 offset = Vector3Add(Vector3Add(scaledRight, scaledUp), scaledDepth); + Vector3 scaledNDCCoord = Vector3Add(centerNDCCube, offset); + ((Vector3 *)ndc->meshes[0].vertices)[i] = TranslateRotateScale(1, scaledNDCCoord, modelPos, modelScale, rotation); + } +} + +static void DrawModelFilled(Model *model, Texture2D texture, float rotation) +{ + if (!(COLOR_MODE() || TEXTURE_MODE())) return; + Color *cacheColors = (Color *)model->meshes[0].colors; + if (TEXTURE_MODE() && !COLOR_MODE()) model->meshes[0].colors = NULL; + + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture.id = (TEXTURE_MODE())? texture.id : 0; + DrawModelEx(*model, modelPos, yAxis, RAD2DEG*rotation, modelScale, WHITE); + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture.id = 0; + + model->meshes[0].colors = (unsigned char *)cacheColors; +} + +static void DrawModelWiresAndPoints(Model *model, float rotation) +{ + Color *cacheColors = (Color *)model->meshes[0].colors; + unsigned int cacheID = model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture.id; + model->meshes[0].colors = NULL; + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture.id = 0; + + DrawModelWiresEx(*model, modelPos, yAxis, RAD2DEG*rotation, modelScale, MARINER); + rlSetPointSize(4.0f); + DrawModelPointsEx(*model, modelPos, yAxis, RAD2DEG*rotation, modelScale, LILAC); + + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture.id = cacheID; + model->meshes[0].colors = (unsigned char *)cacheColors; +} + +static void UpdateSpatialFrame(Camera3D *main, float aspect, float near, float far, Mesh *spatialFrame) +{ + Vector3 depth, right, up; + BasisVector(main, &depth, &right, &up); + float halfHNear = near*tanf(DEG2RAD*main->fovy*0.5f); + float halfWNear = Lerp(halfHNear, halfHNear*aspect, AspectBlendFactor(0.0f)); + float halfHFar = far*tanf(DEG2RAD*main->fovy*0.5f); + float halfWFar = Lerp(halfHFar, halfHFar*aspect, AspectBlendFactor(0.0f)); + float halfDepthNdc = Lerp(halfHNear, 0.5f*(far - near), AspectBlendFactor(0.0f)); + float halfDepth = Lerp(0.5f*(far - near), halfDepthNdc, SpaceBlendFactor(0.0f)); + float farHalfW = Lerp(halfWFar, halfWNear, SpaceBlendFactor(0.0f)); + float farHalfH = Lerp(halfHFar, halfHNear, SpaceBlendFactor(0.0f)); + Vector3 centerNear = Vector3Add(main->position, Vector3Scale(depth, near)); + + for (int i = 0; i < spatialFrame->vertexCount; ++i) + { + Vector3 offset = Vector3Subtract(((Vector3 *)spatialFrame->vertices)[i], centerNear); + float xSign = (Vector3DotProduct(offset, right) >= 0.0f)? 1.0f : -1.0f; + float ySign = (Vector3DotProduct(offset, up) >= 0.0f)? 1.0f : -1.0f; + float farMask = (Vector3DotProduct(offset, depth) > halfDepth)? 1.0f : 0.0f; + float finalHalfW = halfWNear + farMask*(farHalfW - halfWNear); + float finalHalfH = halfHNear + farMask*(farHalfH - halfHNear); + Vector3 center = Vector3Add(centerNear, Vector3Scale(depth, farMask*2.0f*halfDepth)); + ((Vector3 *)spatialFrame->vertices)[i] = Vector3Add(center, Vector3Add(Vector3Scale(right, xSign*finalHalfW), Vector3Scale(up, ySign*finalHalfH))); + } +} + +static void DrawSpatialFrame(Mesh spatialFrame) +{ + static int frontFaces[4][2] = { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 0 } }; + static int backFaces[4][2] = { { 4, 5 }, { 5, 6 }, { 6, 7 }, { 7, 4 } }; + static int ribFaces[4][2] = { { 0, 4 }, { 1, 7 }, { 2, 6 }, { 3, 5 } }; + static int (*faces[3])[2] = { frontFaces, backFaces, ribFaces }; + + for (int i = 0; i < 3; i++) + for (int j = 0; j < 4; j++) + { + Vector3 startPosition = ((Vector3 *)spatialFrame.vertices)[faces[i][j][0]]; + Vector3 endPosition = ((Vector3 *)spatialFrame.vertices)[faces[i][j][1]]; + DrawLine3D(startPosition, endPosition, (i == 0)? NEON_CARROT : (i == 1)? EGGPLANT : HOPBUSH); + } +} + +static void DrawNearPlanePoints(Camera3D *main, float aspect, float near, Model *nearPlanePointsModel, Mesh *mesh, float rotation) +{ + Vector3 depth, right, up; + BasisVector(main, &depth, &right, &up); + int nearPlaneVertexCount = 0; + int capacity = mesh->triangleCount*3; + Mesh *nearPlanePointsMesh = &nearPlanePointsModel->meshes[0]; + Vector3 centerNearPlane = Vector3Add(main->position, Vector3Scale(depth, near)); + float xAspect = Lerp(1.0f/aspect, 1.0f, AspectBlendFactor(0.0f)); + float yReflect = Lerp(1.0f, -1.0f, ReflectBlendFactor(0.0f)); + + for (int i = 0; i < mesh->triangleCount; i++) + { + Vector3 *vertices = (Vector3 *)mesh->vertices; + Triangle *triangles = (Triangle *)mesh->indices; + + Vector3 a = TranslateRotateScale(0, vertices[triangles[i][0]], modelPos, modelScale, rotation); + Vector3 b = TranslateRotateScale(0, vertices[triangles[i][1]], modelPos, modelScale, rotation); + Vector3 c = TranslateRotateScale(0, vertices[triangles[i][2]], modelPos, modelScale, rotation); + + // test if front facing or not (ugly one-liner -- comment out will ~double the rays, which is fine) + if (Vector3DotProduct(Vector3Normalize(Vector3CrossProduct(Vector3Subtract(b, a), Vector3Subtract(c, a))), depth) > 0.0f) continue; + Vector3 intersectionPoints[3] = { Intersect(main, near, a), Intersect(main, near, b), Intersect(main, near, c) }; + + for (int j = 0; j < 3 && nearPlaneVertexCount < capacity; ++j) + { + Vector3 corrected = AspectCorrectAndReflectNearPlane(intersectionPoints[j], centerNearPlane, right, up, xAspect, yReflect); + DrawLine3D((Vector3[]){ a, b, c }[j], corrected, (Color){ RED_DAMASK.r, RED_DAMASK.g, RED_DAMASK.b, 20 }); + ((Vector3 *)nearPlanePointsMesh->vertices)[nearPlaneVertexCount] = corrected; + nearPlaneVertexCount++; + } + } + + nearPlanePointsMesh->vertexCount = nearPlaneVertexCount; + rlSetPointSize(3.0f); + DrawModelPoints(*nearPlanePointsModel, modelPos, 1.0f, LILAC); +} + +static void PerspectiveIncorrectCapture(Camera3D *main, float aspect, float near, Mesh *mesh, Texture2D meshTexture, float rotation) +{ + Vector3 depth, right, up; + BasisVector(main, &depth, &right, &up); + Vector3 centerNearPlane = Vector3Add(main->position, Vector3Scale(depth, near)); + float xAspect = Lerp(1.0f/aspect, 1.0f, AspectBlendFactor(0.0f)); + float yReflect = Lerp(1.0f, -1.0f, ReflectBlendFactor(0.0f)); + + rlColor4ub(WHITE.r, WHITE.g, WHITE.b, WHITE.a); // just to emphasize raylib Colors are ub 0~255 not floats + if (TEXTURE_MODE()) + rlEnableTexture(meshTexture.id); + else + rlDisableTexture(); + + if (!TEXTURE_MODE() && !COLOR_MODE()) + { + rlEnableWireMode(); + rlColor4ub(MARINER.r, MARINER.g, MARINER.b, MARINER.a); + } + rlBegin(RL_TRIANGLES); + + for (int i = 0; i < mesh->triangleCount; i++) + { + Triangle *triangles = (Triangle *)mesh->indices; + Vector3 *vertices = (Vector3 *)mesh->vertices; + Color *colors = (Color *)mesh->colors; + Vector2 *texcoords = (Vector2 *)mesh->texcoords; + + Vector3 a = TranslateRotateScale(0, vertices[triangles[i][0]], modelPos, modelScale, rotation); + Vector3 b = TranslateRotateScale(0, vertices[triangles[i][1]], modelPos, modelScale, rotation); + Vector3 c = TranslateRotateScale(0, vertices[triangles[i][2]], modelPos, modelScale, rotation); + + a = AspectCorrectAndReflectNearPlane(Intersect(main, near, a), centerNearPlane, right, up, xAspect, yReflect); + b = AspectCorrectAndReflectNearPlane(Intersect(main, near, b), centerNearPlane, right, up, xAspect, yReflect); + c = AspectCorrectAndReflectNearPlane(Intersect(main, near, c), centerNearPlane, right, up, xAspect, yReflect); + + if (COLOR_MODE()) rlColor4ub(colors[triangles[i][0]].r, colors[triangles[i][0]].g, colors[triangles[i][0]].b, colors[triangles[i][0]].a); + if (TEXTURE_MODE()) rlTexCoord2f(texcoords[triangles[i][0]].x, texcoords[triangles[i][0]].y); + rlVertex3f(a.x, a.y, a.z); + // vertex winding!! to account for reflection toggle (will draw the inside of the geometry otherwise) + int secondIndex = (NDC_SPACE() && REFLECT_Y())? triangles[i][2] : triangles[i][1]; + Vector3 secondVertex = (NDC_SPACE() && REFLECT_Y())? c : b; + if (COLOR_MODE()) rlColor4ub(colors[secondIndex].r, colors[secondIndex].g, colors[secondIndex].b, colors[secondIndex].a); + if (TEXTURE_MODE()) rlTexCoord2f(texcoords[secondIndex].x, texcoords[secondIndex].y); + rlVertex3f(secondVertex.x, secondVertex.y, secondVertex.z); + + int thirdIndex = (NDC_SPACE() && REFLECT_Y())? triangles[i][1] : triangles[i][2]; + Vector3 thirdVertex = (NDC_SPACE() && REFLECT_Y())? b : c; + if (COLOR_MODE()) rlColor4ub(colors[thirdIndex].r, colors[thirdIndex].g, colors[thirdIndex].b, colors[thirdIndex].a); + if (TEXTURE_MODE()) rlTexCoord2f(texcoords[thirdIndex].x, texcoords[thirdIndex].y); + rlVertex3f(thirdVertex.x, thirdVertex.y, thirdVertex.z); + } + + rlEnd(); + rlDisableTexture(); + rlDisableWireMode(); +} + +static void PerspectiveCorrectCapture(Camera3D *main, Model *model, Texture2D meshTexture, Texture2D *perspectiveCorrectTexture, float rotation) +{ + unsigned char *cacheColors = model->meshes[0].colors; + if (TEXTURE_MODE() && !COLOR_MODE()) model->meshes[0].colors = NULL; + + ClearBackground(BLACK); + + BeginMode3D(*main); + Texture2D previousTexture = model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture; + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture = meshTexture; + DrawModelEx(*model, modelPos, yAxis, RAD2DEG*rotation, modelScale, WHITE); + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture = previousTexture; + EndMode3D(); + + Image rgba = LoadImageFromScreen(); + ImageFormat(&rgba, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + model->meshes[0].colors = cacheColors; + + ClearBackground(BLACK); + + BeginMode3D(*main); + Texture2D cacheTexture = model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture; + Color cacheMaterialColor = model->materials[0].maps[MATERIAL_MAP_ALBEDO].color; + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture = (Texture2D){ 0 }; + model->materials[0].maps[MATERIAL_MAP_ALBEDO].color = WHITE; + DrawModelEx(*model, modelPos, yAxis, RAD2DEG*rotation, modelScale, WHITE); + model->materials[0].maps[MATERIAL_MAP_ALBEDO].texture = cacheTexture; + model->materials[0].maps[MATERIAL_MAP_ALBEDO].color = cacheMaterialColor; + EndMode3D(); + + Image mask = LoadImageFromScreen(); + AlphaMaskPunchOut(&rgba, &mask, 1); + ImageFlipVertical(&rgba); + if ((NDC_SPACE() && REFLECT_Y())) ImageFlipVertical(&rgba); // FLIP AGAIN.. it works visually, but is not clear and feels hacked/ugly + if ((perspectiveCorrectTexture->id != 0)) + UpdateTexture(*perspectiveCorrectTexture, rgba.data); + else + *perspectiveCorrectTexture = LoadTextureFromImage(rgba); + + UnloadImage(mask); + UnloadImage(rgba); +} + +static void OrbitSpace(Camera3D *jugemu, float dt) +{ + float radius = Vector3Length(jugemu->position); + float azimuth = atan2f(jugemu->position.z, jugemu->position.x); + float horizontalRadius = sqrtf(jugemu->position.x*jugemu->position.x + jugemu->position.z*jugemu->position.z); + float elevation = atan2f(jugemu->position.y, horizontalRadius); + if (IsKeyDown(KEY_LEFT)) azimuth += 1.5f*dt; + if (IsKeyDown(KEY_RIGHT)) azimuth -= 1.5f*dt; + if (IsKeyDown(KEY_UP)) elevation += 1.0f*dt; + if (IsKeyDown(KEY_DOWN)) elevation -= 1.0f*dt; + if (IsKeyDown(KEY_W)) radius -= 2.0f*dt; + if (IsKeyDown(KEY_S)) radius += 2.0f*dt; + elevation = Clamp(elevation, -M_PI_2 + 0.1f, M_PI_2 - 0.1f); + jugemu->position.x = Clamp(radius, 0.25f, 10.0f)*cosf(elevation)*cosf(azimuth); + jugemu->position.y = Clamp(radius, 0.25f, 10.0f)*sinf(elevation); + jugemu->position.z = Clamp(radius, 0.25f, 10.0f)*cosf(elevation)*sinf(azimuth); +} + +static void AlphaMaskPunchOut(Image *rgba, Image *mask, unsigned char threshold) +{ + Image maskCopy = ImageCopy(*mask); + ImageFormat(&maskCopy, PIXELFORMAT_UNCOMPRESSED_GRAYSCALE); + ImageFormat(rgba, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + unsigned char *maskGrayScale = maskCopy.data; + Color *colors = rgba->data; + int pixelCount = rgba->width*rgba->height; + for (size_t i = 0; i < pixelCount; ++i) colors[i].a = (maskGrayScale[i] > threshold)? 255 : 0; + UnloadImage(maskCopy); +} + +static void FillVertexColors(Mesh *mesh) +{ + if (!mesh->colors) mesh->colors = RL_CALLOC(mesh->vertexCount, sizeof(Color)); + Color *colors = (Color *)mesh->colors; + Vector3 *vertices = (Vector3 *)mesh->vertices; + BoundingBox bounds = GetMeshBoundingBox(*mesh); + + for (int i = 0; i < mesh->vertexCount; ++i) + { + Vector3 vertex = vertices[i]; + float nx = (vertex.x - 0.5f*(bounds.min.x + bounds.max.x))/(0.5f*(bounds.max.x - bounds.min.x)); + float ny = (vertex.y - 0.5f*(bounds.min.y + bounds.max.y))/(0.5f*(bounds.max.y - bounds.min.y)); + float nz = (vertex.z - 0.5f*(bounds.min.z + bounds.max.z))/(0.5f*(bounds.max.z - bounds.min.z)); + float len = sqrtf(nx*nx + ny*ny + nz*nz); + colors[i] = (Color){ lrintf(127.5f*(nx/len + 1.0f)), lrintf(127.5f*(ny/len + 1.0f)), lrintf(127.5f*(nz/len + 1.0f)), 255 }; + } +} + +static Vector3 AspectCorrectAndReflectNearPlane(Vector3 intersect, Vector3 center, Vector3 right, Vector3 up, float xAspect, float yReflect) +{ + Vector3 centerDistance = Vector3Subtract(intersect, center); + float x = Vector3DotProduct(centerDistance, right); + float y = Vector3DotProduct(centerDistance, up); + return Vector3Add(center, Vector3Add(Vector3Scale(right, x*xAspect), Vector3Scale(up, y*yReflect))); +} + +static Vector3 TranslateRotateScale(int inverse, Vector3 coordinate, Vector3 pos, Vector3 scale, float rotation) +{ + Matrix matrix = MatrixMultiply(MatrixMultiply(MatrixScale(scale.x, scale.y, scale.z), MatrixRotateY(rotation)), MatrixTranslate(pos.x, pos.y, pos.z)); + Matrix result = inverse ? MatrixInvert(matrix) : matrix; + return Vector3Transform(coordinate, result); +} + +static Vector3 Intersect(Camera3D *main, float near, Vector3 worldCoord) +{ + Vector3 viewDir = Vector3Normalize(Vector3Subtract(main->target, main->position)); + Vector3 mainCameraToPoint = Vector3Subtract(worldCoord, main->position); + float depthAlongView = Vector3DotProduct(mainCameraToPoint, viewDir); + if (depthAlongView <= 0.0f) return Vector3Add(main->position, Vector3Scale(viewDir, near)); + float scaleToNear = near/depthAlongView; + return Vector3Add(main->position, Vector3Scale(mainCameraToPoint, scaleToNear)); +} + +static float SpaceBlendFactor(float dt) +{ + static float blend = 0.0f; + if (dt > 0.0f) blend = Clamp(blend + ((NDC_SPACE())? 1.0f : -1.0f)*blendScalar*dt, 0.0f, 1.0f); + return blend; +} + +static float AspectBlendFactor(float dt) +{ + static float blend = 0.0f; + if (dt > 0.0f) blend = Clamp(blend + ((ASPECT_CORRECT())? 1.0f : -1.0f)*blendScalar*dt, 0.0f, 1.0f); + return blend; +} + +static float ReflectBlendFactor(float dt) +{ + static float blend = 0.0f; + if (dt > 0.0f) + { + float target = (NDC_SPACE() && REFLECT_Y())? 1.0f : 0.0f; + float direction = (blend < target)? 1.0f : (blend > target)? -1.0f : 0.0f; + blend = Clamp(blend + direction*blendScalar*dt, 0.0f, 1.0f); + } + return blend; +} diff --git a/examples/core/core_3d_fixed_function_didactic.png b/examples/core/core_3d_fixed_function_didactic.png new file mode 100644 index 0000000000000000000000000000000000000000..c57d5950b35f0be1f67f27e968a6a07c2696f18b GIT binary patch literal 58096 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYU_8XZ#=yWJp1k%11H+^3o-U3d6>-Ta2?;;W z8*sTfCz!BcCmQ$^512@uTC8Xw#e|)3ST7*)wxMU)){6oXyp320#QY8_inbj>{P@#F!S#PPll)43`@WZZS&wGGlk2ffUnWeHOQL2e;s_85})r2e1$f zcbyWpFdNOffjtmFp<}JEI1@x;cjJ%N121?^WUP|I8Ul?M1r|he^prWc3E#$IJJ>}G zcbytiP1d+OCs?!`_&@VlLcYPW`Lewi&;nFZj_LpTGcF$)7kmC%@Zn-Y>Ggo_cNZlm zTvW=!8HIU@4xe~9a+b`D5I^L$qmAF0Z$Z}XJyS|- zVm@o`{MmZ%q2mgfg=PCxuGTLueVf72(<0y?zfsL=&FF3O z&-Q!YjDjTWmKd%1_LHZ%1(li1H0541@7}K!p=mwm8hWnTIdwhT&UX@eFXh8+_^#G? zX~bT-9slH+aMaS%7k8DKu(&00Jebw!FuzhEqV7T_KhxGb-eo_X56s_@Cc$=EuD6lT z-s`hOP8#!c%N=Pp94`;2%I*B6@%1?<-fB&7r7VZ-0ur{Y*%MUEiu?hPCSQq-kQFcY^*H{buYpqa zi;Wc)w|>Nv0uv7SAN=M2g!kGDPz}>y==Pn&c%Fw%>Rqp_59x|q*s(!Mm_HXWibIh#vu0{op*%|ZhYfg@OyH^ zciwF?=S-Zoto@c|#Nyc(+`aFjrEEBtaaN1NEbeEj-OTqG70X;?ZCy@WHIKM_En)7( zgQ9Ue4%M&tnY?3)>0#YFoKN3hEHR7n2HV)v#!&0jF!9LjyY5+gdcFj2*idSekePUG zUZljyq!PB2EiBTwGUowlrX^)YA0*8(tQ5A);#4g6wbiR_Iiq}iU*j!d$CvY%iyrsh z39efaYIY%upKt9cy{zTQCFPH9H5*sn>cthqgk7a1jWx8Y1G8RASY zWO+*Lwml8nQoyTx;llsLg0@mj&bBOW-&uN=J%k4us2sIZY_MBw6p=oS?eWX_FSCv= zP}$o1j!{x~Esj!+qxC?*iIJ)-E-D&WbvyokVL#bSIPYWzd&-srlGqZ#H${V6CnaWRw3XWUq%A!x z3#w0a3$O)STMb8#-c%`@zz&<+jdwma>@3*AY!vkXTeT7(!t`a=&N(+Cdk$7j{#D?g zb|cMt!_P{b>Um}fNW^NHZV^&mc$05ciNnA1CTz10�TW;(}th?M6rcHP#-zUNd%k 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 W)%z1y-DF^3VDNPHb6Mw<&;$Tuk1its literal 0 HcmV?d00001 diff --git a/src/rmodels.c b/src/rmodels.c index 3c904c396..35b086972 100644 --- a/src/rmodels.c +++ b/src/rmodels.c @@ -1435,7 +1435,7 @@ void DrawMesh(Mesh mesh, Material material, Matrix transform) #define GL_COLOR_ARRAY 0x8076 #define GL_TEXTURE_COORD_ARRAY 0x8078 - rlEnableTexture(material.maps[MATERIAL_MAP_DIFFUSE].texture.id); + if (mesh.texcoords && material.maps[MATERIAL_MAP_DIFFUSE].texture.id > 0) rlEnableTexture(material.maps[MATERIAL_MAP_DIFFUSE].texture.id); if (mesh.animVertices) rlEnableStatePointer(GL_VERTEX_ARRAY, mesh.animVertices); else rlEnableStatePointer(GL_VERTEX_ARRAY, mesh.vertices); @@ -4434,10 +4434,12 @@ static Model LoadOBJ(const char *fileName) model.meshes[i].vertices = (float *)MemAlloc(sizeof(float)*vertexCount*3); model.meshes[i].normals = (float *)MemAlloc(sizeof(float)*vertexCount*3); - model.meshes[i].texcoords = (float *)MemAlloc(sizeof(float)*vertexCount*2); #if defined(GRAPHICS_API_OPENGL_33) || defined(GRAPHICS_API_OPENGL_ES2) + model.meshes[i].texcoords = (float *)MemAlloc(sizeof(float)*vertexCount*2); model.meshes[i].colors = (unsigned char *)MemAlloc(sizeof(unsigned char)*vertexCount*4); #else + if (objAttributes.texcoords != NULL && objAttributes.num_texcoords > 0) model.meshes[i].texcoords = (float *)MemAlloc(sizeof(float)*vertexCount*2); + else model.meshes[i].texcoords = NULL; model.meshes[i].colors = NULL; #endif } @@ -4493,16 +4495,11 @@ static Model LoadOBJ(const char *fileName) for (int i = 0; i < 3; i++) model.meshes[meshIndex].vertices[localMeshVertexCount*3 + i] = objAttributes.vertices[vertIndex*3 + i]; - if ((objAttributes.texcoords != NULL) && (texcordIndex != TINYOBJ_INVALID_INDEX) && (texcordIndex >= 0)) + if ((objAttributes.texcoords != NULL) && (texcordIndex != TINYOBJ_INVALID_INDEX) && (texcordIndex >= 0) && (model.meshes[meshIndex].texcoords)) { for (int i = 0; i < 2; i++) model.meshes[meshIndex].texcoords[localMeshVertexCount*2 + i] = objAttributes.texcoords[texcordIndex*2 + i]; model.meshes[meshIndex].texcoords[localMeshVertexCount*2 + 1] = 1.0f - model.meshes[meshIndex].texcoords[localMeshVertexCount*2 + 1]; } - else - { - model.meshes[meshIndex].texcoords[localMeshVertexCount*2 + 0] = 0.0f; - model.meshes[meshIndex].texcoords[localMeshVertexCount*2 + 1] = 0.0f; - } if ((objAttributes.normals != NULL) && (normalIndex != TINYOBJ_INVALID_INDEX) && (normalIndex >= 0)) {