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 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
Gn?zr(U_86p3g;N}z z03#Px{YQ&|!N86xv;pc)|G4I9!R3|>_%*P|q1(-k*PSeh4gaV#$O98f8(0XKqDMeJ z*Po#Bzf+VIgnD+oPQFUZW6a%aapO*j*pWwp&bQ(L{@xJg$29kj5Vlp(ejE z7wxw%n%^@hca`<~D0BCxh=g5Kjlq>9+JW=$@@&Z`Q}kowtM&Z@ifSru$+R0fGM)Cz zEG5W^`e1UnW){%#Xs9RGkO{u>)fXH-xJ)DWFQb>U!!$D5`V?0&DwFDkWM9Ee+;56~ zjv>B~FVC5(HQOdu^ag%b5q8K?F6ee-Q~9isDo zcDCrv#r>@Ay}Ji7pWFjD1roqcHm!ns0{a{2``g>jaM?$9*^#xpH~drwa0#qy&N(|K z(OuY<18wV^xEGN(r*(BWT1ZdC+(YMs=gqVJ@vx~B9Dj0SI@kGi)r0~Vi41_Z)Dj%s zZ-;ww`9$Tv_ylUPf=vMDyJI)ZS!NoN+Vn`1#6bb{-8-wwvSSkrO+qBLUOMC0e%@QH zcv1UkwSnLF>j%4yyKgE<*)gR=b^r6>nGqe z&eRPMY4mLe9B>Q&1&R-h3M;y*6y+-J{$uyiZhh-v+{WKqth-Hz`Fnc1w-1CUS7kiL z^`_ovoHwvI;?5CY`3;ENMjXyy%W=(mGVqTA@bUkbz5YkYDcXptcJ8sqf&YR4`o*2S z-4_f^QL0?%Z=G=IoyXx}zyC$QIY>aJ9$se;75m!A=f6H0q6J9ulOI7s~4;(n@- z<6`vj_;>CBE8hXEfvP_TDsy}^2ZGN0SH9Ux2PSA0$54$bB1_k?-NX#^x~2G^Y>l)L z0B(*(!=1zYTGO$&*{0_Tv3iAcu4su2B6KrIkH&;@8O z$JoX^Lg<2&78T$Btc3F;65vH~ukVCmVt1morFK^Q|NXK^RYXjCkzqQ}@ln7t7^MYV z@Q1sWVfgP!VqOZ#268YZD>>K_n7BN3fB$*;5xM}>5m@elW;81~>HqH^nY#hpAB~q0YNdkk~LC^iBCW$ zpa!-di|_wCl6$m}Y-E>({J+UZJBIE$!kHjBCj|IC{y#Js5R(15wG-&ki5)P=Zz$z{ Usl&Zi2>7?n+{)~s2|42b0b`FYBLDyZ 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)) {