From d8b8c0f3fc8ab3872c65bcfefd258bd6ad4ece51 Mon Sep 17 00:00:00 2001 From: arvyy Date: Fri, 16 Aug 2019 17:09:50 +0300 Subject: [PATCH 01/28] change Camera2D behavior (#945) --- examples/core/core_2d_camera.c | 6 ++---- src/core.c | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/core/core_2d_camera.c b/examples/core/core_2d_camera.c index 81f580ad3..ba432f460 100644 --- a/examples/core/core_2d_camera.c +++ b/examples/core/core_2d_camera.c @@ -42,7 +42,7 @@ int main(void) Camera2D camera = { 0 }; camera.target = (Vector2){ player.x + 20, player.y + 20 }; - camera.offset = (Vector2){ 0, 0 }; + camera.offset = (Vector2){ screenWidth / 2, screenHeight / 2 }; camera.rotation = 0.0f; camera.zoom = 1.0f; @@ -57,12 +57,10 @@ int main(void) if (IsKeyDown(KEY_RIGHT)) { player.x += 2; // Player movement - camera.offset.x -= 2; // Camera displacement with player movement } else if (IsKeyDown(KEY_LEFT)) { player.x -= 2; // Player movement - camera.offset.x += 2; // Camera displacement with player movement } // Camera target follows player @@ -135,4 +133,4 @@ int main(void) //-------------------------------------------------------------------------------------- return 0; -} \ No newline at end of file +} diff --git a/src/core.c b/src/core.c index ac0ddd294..81bed6075 100644 --- a/src/core.c +++ b/src/core.c @@ -1251,11 +1251,24 @@ void BeginMode2D(Camera2D camera) rlLoadIdentity(); // Reset current matrix (MODELVIEW) rlMultMatrixf(MatrixToFloat(screenScaling)); // Apply screen scaling if required - // Camera rotation and scaling is always relative to target + //The camera in world-space is set by + //1. Move it to target + //2. Rotate by -rotation and scale by (1/zoom) + // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), + // not for the camera getting bigger, hence the invert. Same deal with rotation. + //3. Move it by (-offset); + // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) + // we need to do it into opposite direction (inverse transform) + // + //Having camera transform in world-space, inverse of it gives the modelview transform. + //Since (A*B*C)' = C'*B'*A', the modelview is + //1. Move to offset + //2. Rotate and Scale + //3. Move by -target Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); - Matrix matTranslation = MatrixTranslate(camera.offset.x + camera.target.x, camera.offset.y + camera.target.y, 0.0f); + Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); Matrix matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); From ce8d7042c625d45e28755d80f9b39beb1d2a6c5a Mon Sep 17 00:00:00 2001 From: Ray Date: Fri, 16 Aug 2019 16:15:47 +0200 Subject: [PATCH 02/28] PR formatting review --- examples/core/core_2d_camera.c | 14 +++++--------- src/core.c | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/examples/core/core_2d_camera.c b/examples/core/core_2d_camera.c index ba432f460..d6c85079e 100644 --- a/examples/core/core_2d_camera.c +++ b/examples/core/core_2d_camera.c @@ -42,7 +42,7 @@ int main(void) Camera2D camera = { 0 }; camera.target = (Vector2){ player.x + 20, player.y + 20 }; - camera.offset = (Vector2){ screenWidth / 2, screenHeight / 2 }; + camera.offset = (Vector2){ screenWidth/2, screenHeight/2 }; camera.rotation = 0.0f; camera.zoom = 1.0f; @@ -54,14 +54,10 @@ int main(void) { // Update //---------------------------------------------------------------------------------- - if (IsKeyDown(KEY_RIGHT)) - { - player.x += 2; // Player movement - } - else if (IsKeyDown(KEY_LEFT)) - { - player.x -= 2; // Player movement - } + + // Player movement + if (IsKeyDown(KEY_RIGHT)) player.x += 2; + else if (IsKeyDown(KEY_LEFT)) player.x -= 2; // Camera target follows player camera.target = (Vector2){ player.x + 20, player.y + 20 }; diff --git a/src/core.c b/src/core.c index 81bed6075..d4df605f4 100644 --- a/src/core.c +++ b/src/core.c @@ -1251,20 +1251,20 @@ void BeginMode2D(Camera2D camera) rlLoadIdentity(); // Reset current matrix (MODELVIEW) rlMultMatrixf(MatrixToFloat(screenScaling)); // Apply screen scaling if required - //The camera in world-space is set by - //1. Move it to target - //2. Rotate by -rotation and scale by (1/zoom) + // The camera in world-space is set by + // 1. Move it to target + // 2. Rotate by -rotation and scale by (1/zoom) // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), // not for the camera getting bigger, hence the invert. Same deal with rotation. - //3. Move it by (-offset); + // 3. Move it by (-offset); // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) // we need to do it into opposite direction (inverse transform) - // - //Having camera transform in world-space, inverse of it gives the modelview transform. - //Since (A*B*C)' = C'*B'*A', the modelview is - //1. Move to offset - //2. Rotate and Scale - //3. Move by -target + + // Having camera transform in world-space, inverse of it gives the modelview transform. + // Since (A*B*C)' = C'*B'*A', the modelview is + // 1. Move to offset + // 2. Rotate and Scale + // 3. Move by -target Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); From 7ebcd066527c021c8b444cb00f05f4c916cee78b Mon Sep 17 00:00:00 2001 From: Oraoto Date: Mon, 19 Aug 2019 16:25:26 +0800 Subject: [PATCH 03/28] Fix uninitialized vboId in GenMeshHeightmap (#949) --- src/models.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models.c b/src/models.c index 5dce17678..fd139e00b 100644 --- a/src/models.c +++ b/src/models.c @@ -1769,6 +1769,7 @@ Mesh GenMeshHeightmap(Image heightmap, Vector3 size) #define GRAY_VALUE(c) ((c.r+c.g+c.b)/3) Mesh mesh = { 0 }; + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); int mapX = heightmap.width; int mapZ = heightmap.height; From 4b79f63d07ca81c180ce9d021b2d5fa6cb6af401 Mon Sep 17 00:00:00 2001 From: Ray Date: Mon, 19 Aug 2019 12:08:45 +0200 Subject: [PATCH 04/28] new example: shaders_fog --- .../shaders/resources/shaders/glsl330/fog.fs | 99 ++++++++++++ .../shaders/resources/shaders/glsl330/fog.vs | 32 ++++ examples/shaders/shaders_fog.c | 153 ++++++++++++++++++ examples/shaders/shaders_fog.png | Bin 0 -> 81626 bytes 4 files changed, 284 insertions(+) create mode 100644 examples/shaders/resources/shaders/glsl330/fog.fs create mode 100644 examples/shaders/resources/shaders/glsl330/fog.vs create mode 100644 examples/shaders/shaders_fog.c create mode 100644 examples/shaders/shaders_fog.png diff --git a/examples/shaders/resources/shaders/glsl330/fog.fs b/examples/shaders/resources/shaders/glsl330/fog.fs new file mode 100644 index 000000000..57ed14807 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/fog.fs @@ -0,0 +1,99 @@ +#version 330 + +// Input vertex attributes (from vertex shader) +in vec2 fragTexCoord; +in vec4 fragColor; +in vec3 fragPosition; +in vec3 fragNormal; + +// Input uniform values +uniform sampler2D texture0; +uniform vec4 colDiffuse; + +// Output fragment color +out vec4 finalColor; + +// NOTE: Add here your custom variables + +#define MAX_LIGHTS 4 +#define LIGHT_DIRECTIONAL 0 +#define LIGHT_POINT 1 + +struct MaterialProperty { + vec3 color; + int useSampler; + sampler2D sampler; +}; + +struct Light { + int enabled; + int type; + vec3 position; + vec3 target; + vec4 color; +}; + +// Input lighting values +uniform Light lights[MAX_LIGHTS]; +uniform vec4 ambient; +uniform vec3 viewPos; +uniform float fogDensity; + +void main() +{ + // Texel color fetching from texture sampler + vec4 texelColor = texture(texture0, fragTexCoord); + vec3 lightDot = vec3(0.0); + vec3 normal = normalize(fragNormal); + vec3 viewD = normalize(viewPos - fragPosition); + vec3 specular = vec3(0.0); + + // NOTE: Implement here your fragment shader code + + for (int i = 0; i < MAX_LIGHTS; i++) + { + if (lights[i].enabled == 1) + { + vec3 light = vec3(0.0); + if (lights[i].type == LIGHT_DIRECTIONAL) { + light = -normalize(lights[i].target - lights[i].position); + } + if (lights[i].type == LIGHT_POINT) { + light = normalize(lights[i].position - fragPosition); + } + float NdotL = max(dot(normal, light), 0.0); + lightDot += lights[i].color.rgb * NdotL; + + float specCo = 0.0; + if(NdotL > 0.0) + specCo = pow(max(0.0, dot(viewD, reflect(-(light), normal))), 16);//16 =shine + specular += specCo; + + } + } + + finalColor = (texelColor * ((colDiffuse+vec4(specular,1)) * vec4(lightDot, 1.0))); + finalColor += texelColor * (ambient/10.0); + + // Gamma correction + finalColor = pow(finalColor, vec4(1.0/2.2)); + + // Fog calculation + float dist = length(viewPos - fragPosition); + + // these could be parameters... + const vec4 fogColor = vec4(0.5, 0.5, 0.5, 1.0); + //const float fogDensity = 0.16; + + // Exponential fog + float fogFactor = 1.0/exp((dist*fogDensity)*(dist*fogDensity)); + + // Linear fog (less nice) + //const float fogStart = 2.0; + //const float fogEnd = 10.0; + //float fogFactor = (fogEnd - dist)/(fogEnd - fogStart); + + fogFactor = clamp(fogFactor, 0.0, 1.0); + + finalColor = mix(fogColor, finalColor, fogFactor); +} diff --git a/examples/shaders/resources/shaders/glsl330/fog.vs b/examples/shaders/resources/shaders/glsl330/fog.vs new file mode 100644 index 000000000..00779cfa3 --- /dev/null +++ b/examples/shaders/resources/shaders/glsl330/fog.vs @@ -0,0 +1,32 @@ +#version 330 + +// Input vertex attributes +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; +in vec4 vertexColor; + +// Input uniform values +uniform mat4 mvp; +uniform mat4 matModel; + +// Output vertex attributes (to fragment shader) +out vec2 fragTexCoord; +out vec4 fragColor; +out vec3 fragPosition; +out vec3 fragNormal; + +// NOTE: Add here your custom variables + +void main() +{ + // Send vertex attributes to fragment shader + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + fragPosition = vec3(matModel*vec4(vertexPosition, 1.0f)); + mat3 normalMatrix = transpose(inverse(mat3(matModel))); + fragNormal = normalize(normalMatrix*vertexNormal); + + // Calculate final vertex position + gl_Position = mvp*vec4(vertexPosition, 1.0); +} diff --git a/examples/shaders/shaders_fog.c b/examples/shaders/shaders_fog.c new file mode 100644 index 000000000..e8f2691d2 --- /dev/null +++ b/examples/shaders/shaders_fog.c @@ -0,0 +1,153 @@ +/******************************************************************************************* +* +* raylib [shaders] example - fog +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version. +* +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3). +* +* This example has been created using raylib 2.5 (www.raylib.com) +* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* +* Example contributed by Chris Camacho (@codifies) and reviewed by Ramon Santamaria (@raysan5) +* +* Chris Camacho (@codifies - http://bedroomcoders.co.uk/) notes: +* +* This is based on the PBR lighting example, but greatly simplified to aid learning... +* actually there is very little of the PBR example left! +* When I first looked at the bewildering complexity of the PBR example I feared +* I would never understand how I could do simple lighting with raylib however its +* a testement to the authors of raylib (including rlights.h) that the example +* came together fairly quickly. +* +* Copyright (c) 2019 Chris Camacho (@codifies) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" + +#include "raymath.h" + +#define RLIGHTS_IMPLEMENTATION +#include "rlights.h" + +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + SetConfigFlags(FLAG_MSAA_4X_HINT); // Enable Multi Sampling Anti Aliasing 4x (if available) + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - fog"); + + // Define the camera to look into our 3d world + Camera camera = { + (Vector3){ 2.0f, 2.0f, 6.0f }, // position + (Vector3){ 0.0f, 0.5f, 0.0f }, // target + (Vector3){ 0.0f, 1.0f, 0.0f }, // up + 45.0f, CAMERA_PERSPECTIVE }; // fov, type + + // Load models and texture + Model modelA = LoadModelFromMesh(GenMeshTorus(0.4f, 1.0f, 16, 32)); + Model modelB = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + Model modelC = LoadModelFromMesh(GenMeshSphere(0.5f, 32, 32)); + Texture texture = LoadTexture("resources/texel_checker.png"); + + // Assign texture to default model material + modelA.materials[0].maps[MAP_DIFFUSE].texture = texture; + modelB.materials[0].maps[MAP_DIFFUSE].texture = texture; + modelC.materials[0].maps[MAP_DIFFUSE].texture = texture; + + // Load shader and set up some uniforms + Shader shader = LoadShader("resources/shaders/glsl330/fog.vs", "resources/shaders/glsl330/fog.fs"); + shader.locs[LOC_MATRIX_MODEL] = GetShaderLocation(shader, "matModel"); + shader.locs[LOC_VECTOR_VIEW] = GetShaderLocation(shader, "viewPos"); + + // Ambient light level + int ambientLoc = GetShaderLocation(shader, "ambient"); + SetShaderValue(shader, ambientLoc, (float[4]){ 0.2f, 0.2f, 0.2f, 1.0f }, UNIFORM_VEC4); + + float fogDensity = 0.15f; + int fogDensityLoc = GetShaderLocation(shader, "fogDensity"); + SetShaderValue(shader, fogDensityLoc, &fogDensity, UNIFORM_FLOAT); + + // NOTE: All models share the same shader + modelA.materials[0].shader = shader; + modelB.materials[0].shader = shader; + modelC.materials[0].shader = shader; + + // Using just 1 point lights + CreateLight(LIGHT_POINT, (Vector3){ 0, 2, 6 }, Vector3Zero(), WHITE, shader); + + SetCameraMode(camera, CAMERA_ORBITAL); // Set an orbital camera mode + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(&camera); // Update camera + + if (IsKeyDown(KEY_UP)) + { + fogDensity += 0.001; + if (fogDensity > 1.0) fogDensity = 1.0; + } + + if (IsKeyDown(KEY_DOWN)) + { + fogDensity -= 0.001; + if (fogDensity < 0.0) fogDensity = 0.0; + } + + SetShaderValue(shader, fogDensityLoc, &fogDensity, UNIFORM_FLOAT); + + // Rotate the torus + modelA.transform = MatrixMultiply(modelA.transform, MatrixRotateX(-0.025)); + modelA.transform = MatrixMultiply(modelA.transform, MatrixRotateZ(0.012)); + + // Update the light shader with the camera view position + SetShaderValue(shader, shader.locs[LOC_VECTOR_VIEW], &camera.position.x, UNIFORM_VEC3); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(GRAY); + + BeginMode3D(camera); + + // Draw the three models + DrawModel(modelA, Vector3Zero(), 1.0f, WHITE); + DrawModel(modelB, (Vector3){ -2.6, 0, 0 }, 1.0f, WHITE); + DrawModel(modelC, (Vector3){ 2.6, 0, 0 }, 1.0f, WHITE); + + for (int i = -20; i < 20; i += 2) DrawModel(modelA,(Vector3){ i, 0, 2 }, 1.0f, WHITE); + + EndMode3D(); + + DrawText(TextFormat("Use KEY_UP/KEY_DOWN to change fog density [%.2f]", fogDensity), 10, 10, 20, RAYWHITE); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadModel(modelA); // Unload the model A + UnloadModel(modelB); // Unload the model B + UnloadModel(modelC); // Unload the model C + UnloadTexture(texture); // Unload the texture + UnloadShader(shader); // Unload shader + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} diff --git a/examples/shaders/shaders_fog.png b/examples/shaders/shaders_fog.png new file mode 100644 index 0000000000000000000000000000000000000000..52008e8a1314fc9c700ff34471806daa105ae158 GIT binary patch literal 81626 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 literal 0 HcmV?d00001 From 973d32f9a73c921dde2d75a6cc3fa9ee111e3e11 Mon Sep 17 00:00:00 2001 From: Ray Date: Mon, 19 Aug 2019 12:17:02 +0200 Subject: [PATCH 05/28] Reviewed issue #943 --- src/models.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models.c b/src/models.c index fd139e00b..711576da2 100644 --- a/src/models.c +++ b/src/models.c @@ -3419,7 +3419,7 @@ static Model LoadGLTF(const char *fileName) Texture2D texture = { 0 }; const char *texPath = GetDirectoryPath(fileName); - if (data->materials[i].pbr_metallic_roughness.base_color_factor) + if (data->materials[i].has_pbr_metallic_roughness) { tint.r = (unsigned char)(data->materials[i].pbr_metallic_roughness.base_color_factor[0]*255.99f); tint.g = (unsigned char)(data->materials[i].pbr_metallic_roughness.base_color_factor[1]*255.99f); @@ -3428,13 +3428,13 @@ static Model LoadGLTF(const char *fileName) } else { - tint.r = 1.f; - tint.g = 1.f; - tint.b = 1.f; - tint.a = 1.f; + tint.r = 1.0f; + tint.g = 1.0f; + tint.b = 1.0f; + tint.a = 1.0f; } - if (data->materials[i].pbr_metallic_roughness.base_color_texture.texture) + if (data->materials[i].has_pbr_metallic_roughness) { cgltf_image *img = data->materials[i].pbr_metallic_roughness.base_color_texture.texture->image; From 12bcdb977a15a4db933765b3cf4af205a492c8b2 Mon Sep 17 00:00:00 2001 From: Colday96 <34099856+Colday96@users.noreply.github.com> Date: Mon, 19 Aug 2019 12:32:17 +0100 Subject: [PATCH 06/28] Update shapes.c for smoother collision detection (#946) By removing the equal sign, if the 2 rects a right next to each other and not overlapping, there will be no collision detection. This is what a majority of other game libraries do and would make it easier to implement collisions for tile based games. --- src/shapes.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapes.c b/src/shapes.c index 6217d2ada..a9fafccc7 100644 --- a/src/shapes.c +++ b/src/shapes.c @@ -1392,8 +1392,8 @@ bool CheckCollisionRecs(Rectangle rec1, Rectangle rec2) { bool collision = false; - if ((rec1.x <= (rec2.x + rec2.width) && (rec1.x + rec1.width) >= rec2.x) && - (rec1.y <= (rec2.y + rec2.height) && (rec1.y + rec1.height) >= rec2.y)) collision = true; + if ((rec1.x < (rec2.x + rec2.width) && (rec1.x + rec1.width) > rec2.x) && + (rec1.y < (rec2.y + rec2.height) && (rec1.y + rec1.height) > rec2.y)) collision = true; return collision; } From 2f42b0ce857ff07ad8a9b50e1a2c24bf0d8bf3c1 Mon Sep 17 00:00:00 2001 From: Ray Date: Mon, 19 Aug 2019 15:09:54 +0200 Subject: [PATCH 07/28] REVIEW: TextSplit() Just adding a security check --- src/text.c | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/text.c b/src/text.c index 1c775c8db..79e46d0c2 100644 --- a/src/text.c +++ b/src/text.c @@ -1372,20 +1372,25 @@ const char **TextSplit(const char *text, char delimiter, int *count) memset(buffer, 0, MAX_TEXT_BUFFER_LENGTH); result[0] = buffer; - int counter = 1; - - // Count how many substrings we have on text and point to every one - for (int i = 0; i < MAX_TEXT_BUFFER_LENGTH; i++) + int counter = 0; + + if (text != NULL) { - buffer[i] = text[i]; - if (buffer[i] == '\0') break; - else if (buffer[i] == delimiter) + counter = 1; + + // Count how many substrings we have on text and point to every one + for (int i = 0; i < MAX_TEXT_BUFFER_LENGTH; i++) { - buffer[i] = '\0'; // Set an end of string at this point - result[counter] = buffer + i + 1; - counter++; + buffer[i] = text[i]; + if (buffer[i] == '\0') break; + else if (buffer[i] == delimiter) + { + buffer[i] = '\0'; // Set an end of string at this point + result[counter] = buffer + i + 1; + counter++; - if (counter == MAX_SUBSTRINGS_COUNT) break; + if (counter == MAX_SUBSTRINGS_COUNT) break; + } } } From 3414910809500f2d9cf2eb5b00df7ecba1dfa9b5 Mon Sep 17 00:00:00 2001 From: Ray Date: Tue, 20 Aug 2019 13:15:10 +0200 Subject: [PATCH 08/28] Update README.md --- projects/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/README.md b/projects/README.md index 4009a09b9..c0fa2442f 100644 --- a/projects/README.md +++ b/projects/README.md @@ -4,14 +4,17 @@ This folder contains raylib templates for some common IDEs. IDE | Platform | Template type | State ----| ---------| ------------- | ----- +[4coder](http://4coder.net/) | Windows | example compiling | DONE [Builder](https://wiki.gnome.org/Apps/Builder) | Linux | example compiling | DONE [CMake](https://cmake.org/) | n/a | example compiling and raylib source downloading/building if necessary | DONE [CodeBlocks](http://www.codeblocks.org/) | Linux, Windows | example compiling | DONE -[Geany](https://www.geany.org/) | Linux, Windows | - | INCOMPLETE +[Geany](https://www.geany.org/) | Linux, Windows | - | DONE [KDevelop](https://www.kdevelop.org/) | Linux, Windows, macOS | - | INCOMPLETE [Notepad++](https://notepad-plus-plus.org/) | Windows | source/example compiling | DONE +[Sublime Text](https://www.sublimetext.com/) | source and example | DONE [VS2015](https://www.visualstudio.com) | Windows | source/example compiling | DONE [VS2017](https://www.visualstudio.com) | Windows | source/example compiling | DONE [VSCode](https://code.visualstudio.com/) | Windows, macOS | example compiling | DONE +scripts | Windows, Linux, macOS | source and example | DONE *New IDEs config files are welcome!* From 64575e711b1fc6196c676a22c42ac436fd767d9e Mon Sep 17 00:00:00 2001 From: Ray Date: Tue, 20 Aug 2019 13:15:48 +0200 Subject: [PATCH 09/28] Update README.md --- projects/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/README.md b/projects/README.md index c0fa2442f..2f0e0f3d1 100644 --- a/projects/README.md +++ b/projects/README.md @@ -11,7 +11,7 @@ IDE | Platform | Template type | State [Geany](https://www.geany.org/) | Linux, Windows | - | DONE [KDevelop](https://www.kdevelop.org/) | Linux, Windows, macOS | - | INCOMPLETE [Notepad++](https://notepad-plus-plus.org/) | Windows | source/example compiling | DONE -[Sublime Text](https://www.sublimetext.com/) | source and example | DONE +[Sublime Text](https://www.sublimetext.com/) | Windows, Linux, macOS | source and example | DONE [VS2015](https://www.visualstudio.com) | Windows | source/example compiling | DONE [VS2017](https://www.visualstudio.com) | Windows | source/example compiling | DONE [VSCode](https://code.visualstudio.com/) | Windows, macOS | example compiling | DONE From ef3b01dffc964c04b83d08211ff133e22af6059b Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 20 Aug 2019 19:32:12 +0200 Subject: [PATCH 10/28] RPI: Corrected issue when creating 3d projection view --- src/core.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core.c b/src/core.c index d4df605f4..2e9bfb635 100644 --- a/src/core.c +++ b/src/core.c @@ -2507,9 +2507,6 @@ static bool InitGraphicsDevice(int width, int height) if (screenHeight <= 0) screenHeight = displayHeight; #endif // PLATFORM_DESKTOP - currentWidth = screenWidth; - currentHeight = screenHeight; - #if defined(PLATFORM_WEB) displayWidth = screenWidth; displayHeight = screenHeight; @@ -3040,6 +3037,9 @@ static bool InitGraphicsDevice(int width, int height) // Setup default viewport SetupViewport(fbWidth, fbHeight); + currentWidth = screenWidth; + currentHeight = screenHeight; + ClearBackground(RAYWHITE); // Default background color for raylib games :P #if defined(PLATFORM_ANDROID) From a727f5768f6e588d45e47dd26da9af82597ba1d3 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 20 Aug 2019 19:33:46 +0200 Subject: [PATCH 11/28] Review OpenGL ES 2.0 extensions check --- src/rlgl.h | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/rlgl.h b/src/rlgl.h index 5e7312bde..9771d30f7 100644 --- a/src/rlgl.h +++ b/src/rlgl.h @@ -1529,24 +1529,33 @@ void rlglInit(int width, int height) const char **extList = RL_MALLOC(sizeof(const char *)*numExt); // Get extensions strings - for (int i = 0; i < numExt; i++) extList[i] = (char *)glGetStringi(GL_EXTENSIONS, i); + for (int i = 0; i < numExt; i++) extList[i] = (const char *)glGetStringi(GL_EXTENSIONS, i); #elif defined(GRAPHICS_API_OPENGL_ES2) // Allocate 512 strings pointers (2 KB) const char **extList = RL_MALLOC(sizeof(const char *)*512); + + const char *extensions = (const char *)glGetString(GL_EXTENSIONS); // One big const string + + // NOTE: We have to duplicate string because glGetString() returns a const string + int len = strlen(extensions) + 1; + char *extensionsDup = (char *)RL_CALLOC(len, 1); + strcpy(extensionsDup, extensions); - // Get extensions strings - char *extensions = (char *)glGetString(GL_EXTENSIONS); // One big static const string returned - int len = strlen(extensions); + extList[numExt] = extensionsDup; for (int i = 0; i < len; i++) { - if (i == ' ') + if (extensionsDup[i] == ' ') { - extList[numExt] = &extensions[i + 1]; + extensionsDup[i] = '\0'; + numExt++; + extList[numExt] = &extensionsDup[i + 1]; } } + + // NOTE: Duplicated string (extensionsDup) must be deallocated #endif TraceLog(LOG_INFO, "Number of supported extensions: %i", numExt); @@ -1622,6 +1631,8 @@ void rlglInit(int width, int height) RL_FREE(extList); #if defined(GRAPHICS_API_OPENGL_ES2) + RL_FREE(extensionsDup); // Duplicated string must be deallocated + if (vaoSupported) TraceLog(LOG_INFO, "[EXTENSION] VAO extension detected, VAO functions initialized successfully"); else TraceLog(LOG_WARNING, "[EXTENSION] VAO extension not found, VAO usage not supported"); From 3db13edd898adcd41aa0768e2e707256687a90ac Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 20 Aug 2019 20:39:22 +0200 Subject: [PATCH 12/28] Support mouse cursor on RPI native Reduced bunnymark limits --- examples/textures/textures_bunnymark.c | 2 +- src/config.h | 2 ++ src/core.c | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/textures/textures_bunnymark.c b/examples/textures/textures_bunnymark.c index 784417d21..86605b901 100644 --- a/examples/textures/textures_bunnymark.c +++ b/examples/textures/textures_bunnymark.c @@ -13,7 +13,7 @@ #include // Required for: malloc(), free() -#define MAX_BUNNIES 100000 // 100K bunnies limit +#define MAX_BUNNIES 50000 // 50K bunnies limit // This is the maximum amount of elements (quads) per batch // NOTE: This value is defined in [rlgl] module and can be changed there diff --git a/src/config.h b/src/config.h index 050cce183..398b5e9ad 100644 --- a/src/config.h +++ b/src/config.h @@ -44,6 +44,8 @@ #define SUPPORT_MOUSE_GESTURES 1 // Reconfigure standard input to receive key inputs, works with SSH connection. #define SUPPORT_SSH_KEYBOARD_RPI 1 +// Draw a mouse reference on screen (square cursor box) +#define SUPPORT_MOUSE_CURSOR_RPI 1 // Use busy wait loop for timing sync, if not defined, a high-resolution timer is setup and used //#define SUPPORT_BUSY_WAIT_LOOP 1 // Wait for events passively (sleeping while no events) instead of polling them actively every frame diff --git a/src/core.c b/src/core.c index 2e9bfb635..400712051 100644 --- a/src/core.c +++ b/src/core.c @@ -55,6 +55,9 @@ * WARNING: Reconfiguring standard input could lead to undesired effects, like breaking other running processes or * blocking the device is not restored properly. Use with care. * +* #define SUPPORT_MOUSE_CURSOR_RPI (Raspberry Pi only) +* Draw a mouse reference on screen (square cursor box) +* * #define SUPPORT_BUSY_WAIT_LOOP * Use busy wait loop for timing sync, if not defined, a high-resolution timer is setup and used * @@ -1187,6 +1190,12 @@ void BeginDrawing(void) // End canvas drawing and swap buffers (double buffering) void EndDrawing(void) { +#if defined(PLATFORM_RPI) && defined(SUPPORT_MOUSE_CURSOR_RPI) + // On RPI native mode we have no system mouse cursor, so, + // we draw a small rectangle for user reference + DrawRectangle(mousePosition.x, mousePosition.y, 3, 3, MAROON); +#endif + rlglDraw(); // Draw Buffers (Only OpenGL 3+ and ES2) #if defined(SUPPORT_GIF_RECORDING) From 20257f3e49a25430912330caa98e06b81b4276b3 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 20 Aug 2019 21:17:06 +0200 Subject: [PATCH 13/28] Uniform initialization not supported on GLSL 100 --- examples/core/resources/distortion100.fs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/core/resources/distortion100.fs b/examples/core/resources/distortion100.fs index 6bfe252b3..112cbcb5c 100644 --- a/examples/core/resources/distortion100.fs +++ b/examples/core/resources/distortion100.fs @@ -11,14 +11,14 @@ uniform sampler2D texture0; uniform vec4 colDiffuse; // NOTE: Add here your custom variables -uniform vec2 leftLensCenter = vec2(0.288, 0.5); -uniform vec2 rightLensCenter = vec2(0.712, 0.5); -uniform vec2 leftScreenCenter = vec2(0.25, 0.5); -uniform vec2 rightScreenCenter = vec2(0.75, 0.5); -uniform vec2 scale = vec2(0.25, 0.45); -uniform vec2 scaleIn = vec2(4, 2.2222); -uniform vec4 hmdWarpParam = vec4(1, 0.22, 0.24, 0); -uniform vec4 chromaAbParam = vec4(0.996, -0.004, 1.014, 0.0); +uniform vec2 leftLensCenter; +uniform vec2 rightLensCenter; +uniform vec2 leftScreenCenter; +uniform vec2 rightScreenCenter; +uniform vec2 scale; +uniform vec2 scaleIn; +uniform vec4 hmdWarpParam; +uniform vec4 chromaAbParam; void main() { From 20fa362700cd5eefb9eeb196711dfbec5a73f8ab Mon Sep 17 00:00:00 2001 From: raysan5 Date: Wed, 21 Aug 2019 18:59:15 +0200 Subject: [PATCH 14/28] Review comment --- src/core.c | 4 ++-- src/raylib.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core.c b/src/core.c index 400712051..4352a3214 100644 --- a/src/core.c +++ b/src/core.c @@ -1811,10 +1811,10 @@ const char *GetFileName(const char *filePath) return fileName + 1; } -// Get filename string without extension (memory should be freed) +// Get filename string without extension (uses static string) const char *GetFileNameWithoutExt(const char *filePath) { - #define MAX_FILENAMEWITHOUTEXT_LENGTH 64 + #define MAX_FILENAMEWITHOUTEXT_LENGTH 128 static char fileName[MAX_FILENAMEWITHOUTEXT_LENGTH]; memset(fileName, 0, MAX_FILENAMEWITHOUTEXT_LENGTH); diff --git a/src/raylib.h b/src/raylib.h index 6cf6fd23b..0594b3700 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -939,7 +939,7 @@ RLAPI bool IsFileExtension(const char *fileName, const char *ext);// Check file RLAPI bool DirectoryExists(const char *dirPath); // Check if a directory path exists RLAPI const char *GetExtension(const char *fileName); // Get pointer to extension for a filename string RLAPI const char *GetFileName(const char *filePath); // Get pointer to filename for a path string -RLAPI const char *GetFileNameWithoutExt(const char *filePath); // Get filename string without extension (memory should be freed) +RLAPI const char *GetFileNameWithoutExt(const char *filePath); // Get filename string without extension (uses static string) RLAPI const char *GetDirectoryPath(const char *filePath); // Get full path for a given fileName with path (uses static string) RLAPI const char *GetPrevDirectoryPath(const char *dirPath); // Get previous directory path for a given path (uses static string) RLAPI const char *GetWorkingDirectory(void); // Get current working directory (uses static string) From 061294e4d174f684c77d479e4fb461f4bff844ef Mon Sep 17 00:00:00 2001 From: Ray Date: Sat, 24 Aug 2019 10:11:32 +0200 Subject: [PATCH 15/28] Update miniaudio.h --- src/external/miniaudio.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/external/miniaudio.h b/src/external/miniaudio.h index 528ff5746..7eb4beeeb 100644 --- a/src/external/miniaudio.h +++ b/src/external/miniaudio.h @@ -1,6 +1,6 @@ /* Audio playback and capture library. Choice of public domain or MIT-0. See license statements at the end of this file. -miniaudio (formerly mini_al) - v0.9.6 - 2019-xx-xx +miniaudio (formerly mini_al) - v0.9.6 - 2019-08-04 David Reid - davidreidsoftware@gmail.com @@ -33361,7 +33361,7 @@ Device /* REVISION HISTORY ================ -v0.9.6 - 2019-xx-xx +v0.9.6 - 2019-08-04 - Add support for loading decoders using a wchar_t string for file paths. - Don't trigger an assert when ma_device_start() is called on a device that is already started. This will now log a warning and return MA_INVALID_OPERATION. The same applies for ma_device_stop(). From e67c842faa67123b65e2e2c5093c8d532f7bf130 Mon Sep 17 00:00:00 2001 From: Ray Date: Sat, 24 Aug 2019 20:03:47 +0200 Subject: [PATCH 16/28] Tweak on atlas size computing Some generated fonts were not fitting the image... --- src/text.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text.c b/src/text.c index 79e46d0c2..4eb1b4b04 100644 --- a/src/text.c +++ b/src/text.c @@ -586,7 +586,7 @@ Image GenImageFontAtlas(const CharInfo *chars, Rectangle **charRecs, int charsCo *charRecs = NULL; // In case no chars count provided we suppose default of 95 - charsCount = (charsCount > 0) ? charsCount : 95; + charsCount = (charsCount > 0)? charsCount : 95; // NOTE: Rectangles memory is loaded here! Rectangle *recs = (Rectangle *)RL_MALLOC(charsCount*sizeof(Rectangle)); @@ -597,7 +597,7 @@ Image GenImageFontAtlas(const CharInfo *chars, Rectangle **charRecs, int charsCo // so image size would result bigger than default font type float requiredArea = 0; for (int i = 0; i < charsCount; i++) requiredArea += ((chars[i].image.width + 2*padding)*(chars[i].image.height + 2*padding)); - float guessSize = sqrtf(requiredArea)*1.25f; + float guessSize = sqrtf(requiredArea)*1.3f; int imageSize = (int)powf(2, ceilf(logf((float)guessSize)/logf(2))); // Calculate next POT atlas.width = imageSize; // Atlas bitmap width From 8e86f3586d40992d2cd38416d3b67413f42356c1 Mon Sep 17 00:00:00 2001 From: Zatherz Date: Mon, 26 Aug 2019 00:34:54 +0200 Subject: [PATCH 17/28] IsAudioBufferProcessed -> IsAudioStreamProcessed (#951) It appears that you've missed renaming this function here, which makes trying to compile standalone raudio.c error out. --- src/raudio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/raudio.h b/src/raudio.h index 8bbbe8613..302993a9f 100644 --- a/src/raudio.h +++ b/src/raudio.h @@ -182,7 +182,7 @@ float GetMusicTimePlayed(Music music); // Get current m AudioStream InitAudioStream(unsigned int sampleRate, unsigned int sampleSize, unsigned int channels); // Init audio stream (to stream raw audio pcm data) void UpdateAudioStream(AudioStream stream, const void *data, int samplesCount); // Update audio stream buffers with data void CloseAudioStream(AudioStream stream); // Close audio stream and free memory -bool IsAudioBufferProcessed(AudioStream stream); // Check if any audio stream buffers requires refill +bool IsAudioStreamProcessed(AudioStream stream); // Check if any audio stream buffers requires refill void PlayAudioStream(AudioStream stream); // Play audio stream void PauseAudioStream(AudioStream stream); // Pause audio stream void ResumeAudioStream(AudioStream stream); // Resume audio stream From addbd88833ffc9a2bd1fe33a83e683245324abd0 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Mon, 26 Aug 2019 21:09:03 +0200 Subject: [PATCH 18/28] Expose scissor functionality --- src/core.c | 17 +++++++++++++++++ src/raylib.h | 4 ++-- src/rlgl.h | 51 ++++++++++++++++----------------------------------- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/core.c b/src/core.c index 4352a3214..bc633e8b7 100644 --- a/src/core.c +++ b/src/core.c @@ -1392,6 +1392,23 @@ void EndTextureMode(void) currentHeight = GetScreenHeight(); } +// Begin scissor mode (define screen area for following drawing) +// NOTE: Scissor rec refers to bottom-left corner, we change it to upper-left +void BeginScissorMode(int x, int y, int width, int height) +{ + rlglDraw(); // Force drawing elements + + rlEnableScissorTest(); + rlScissor(x, GetScreenHeight() - (y + height), width, height); +} + +// End scissor mode +void EndScissorMode(void) +{ + rlglDraw(); // Force drawing elements + rlDisableScissorTest(); +} + // Returns a ray trace from mouse position Ray GetMouseRay(Vector2 mousePosition, Camera camera) { diff --git a/src/raylib.h b/src/raylib.h index 0594b3700..56b1f95ea 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -904,6 +904,8 @@ RLAPI void BeginMode3D(Camera3D camera); // Initializes RLAPI void EndMode3D(void); // Ends 3D mode and returns to default 2D orthographic mode RLAPI void BeginTextureMode(RenderTexture2D target); // Initializes render texture for drawing RLAPI void EndTextureMode(void); // Ends drawing to render texture +RLAPI void BeginScissorMode(int x, int y, int width, int height); // Begin scissor mode (define screen area for following drawing) +RLAPI void EndScissorMode(void); // End scissor mode // Screen-space-related functions RLAPI Ray GetMouseRay(Vector2 mousePosition, Camera camera); // Returns a ray trace from mouse position @@ -1324,8 +1326,6 @@ RLAPI void BeginShaderMode(Shader shader); // Beg RLAPI void EndShaderMode(void); // End custom shader drawing (use default shader) RLAPI void BeginBlendMode(int mode); // Begin blending mode (alpha, additive, multiplied) RLAPI void EndBlendMode(void); // End blending mode (reset to default: alpha blending) -RLAPI void BeginScissorMode(int x, int y, int width, int height); // Begin scissor mode (define screen area for following drawing) -RLAPI void EndScissorMode(void); // End scissor mode // VR control functions RLAPI void InitVrSimulator(void); // Init VR simulator for selected device parameters diff --git a/src/rlgl.h b/src/rlgl.h index 9771d30f7..c71033cb0 100644 --- a/src/rlgl.h +++ b/src/rlgl.h @@ -457,6 +457,9 @@ RLAPI void rlEnableDepthTest(void); // Enable depth te RLAPI void rlDisableDepthTest(void); // Disable depth test RLAPI void rlEnableBackfaceCulling(void); // Enable backface culling RLAPI void rlDisableBackfaceCulling(void); // Disable backface culling +RLAPI void rlEnableScissorTest(void); // Enable scissor test +RLAPI void rlDisableScissorTest(void); // Disable scissor test +RLAPI void rlScissor(int x, int y, int width, int height); // Scissor test RLAPI void rlEnableWireMode(void); // Enable wire mode RLAPI void rlDisableWireMode(void); // Disable wire mode RLAPI void rlDeleteTextures(unsigned int id); // Delete OpenGL texture from GPU @@ -1344,28 +1347,25 @@ void rlDisableRenderTexture(void) } // Enable depth test -void rlEnableDepthTest(void) -{ - glEnable(GL_DEPTH_TEST); -} +void rlEnableDepthTest(void) { glEnable(GL_DEPTH_TEST); } // Disable depth test -void rlDisableDepthTest(void) -{ - glDisable(GL_DEPTH_TEST); -} +void rlDisableDepthTest(void) { glDisable(GL_DEPTH_TEST); } // Enable backface culling -void rlEnableBackfaceCulling(void) -{ - glEnable(GL_CULL_FACE); -} +void rlEnableBackfaceCulling(void) { glEnable(GL_CULL_FACE); } // Disable backface culling -void rlDisableBackfaceCulling(void) -{ - glDisable(GL_CULL_FACE); -} +void rlDisableBackfaceCulling(void) { glDisable(GL_CULL_FACE); } + +// Enable scissor test +RLAPI void rlEnableScissorTest(void) { glEnable(GL_SCISSOR_TEST); } + +// Disable scissor test +RLAPI void rlDisableScissorTest(void) { glDisable(GL_SCISSOR_TEST); } + +// Scissor test +RLAPI void rlScissor(int x, int y, int width, int height) { glScissor(x, y, width, height); } // Enable wire mode void rlEnableWireMode(void) @@ -1698,7 +1698,6 @@ void rlglInit(int width, int height) // Initialize OpenGL default states //---------------------------------------------------------- - // Init state: Depth test glDepthFunc(GL_LEQUAL); // Type of depth testing to apply glDisable(GL_DEPTH_TEST); // Disable depth testing for 2D (only used for 3D) @@ -3524,24 +3523,6 @@ void EndBlendMode(void) BeginBlendMode(BLEND_ALPHA); } -// Begin scissor mode (define screen area for following drawing) -// NOTE: Scissor rec refers to bottom-left corner, we change it to upper-left -void BeginScissorMode(int x, int y, int width, int height) -{ - rlglDraw(); // Force drawing elements - - glEnable(GL_SCISSOR_TEST); - glScissor(x, framebufferHeight - (y + height), width, height); -} - -// End scissor mode -void EndScissorMode(void) -{ - rlglDraw(); // Force drawing elements - - glDisable(GL_SCISSOR_TEST); -} - #if defined(SUPPORT_VR_SIMULATOR) // Init VR simulator for selected device parameters // NOTE: It modifies the global variable: stereoFbo From 37a6f120377e28d5499f130ff346848a3983bee8 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Mon, 26 Aug 2019 21:19:04 +0200 Subject: [PATCH 19/28] Unload model shaders and textures They are not automatically unloaded when unloading the model! --- examples/models/models_animation.c | 2 ++ examples/models/models_material_pbr.c | 4 +++- examples/models/models_mesh_generation.c | 3 ++- examples/models/models_obj_viewer.c | 1 + examples/models/models_skybox.c | 5 ++++- examples/models/models_yaw_pitch_roll.c | 1 + 6 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/models/models_animation.c b/examples/models/models_animation.c index 5e7c8cf54..2aa321cf7 100644 --- a/examples/models/models_animation.c +++ b/examples/models/models_animation.c @@ -98,6 +98,8 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- + UnloadTexture(texture); // Unload texture + // Unload model animations data for (int i = 0; i < animsCount; i++) UnloadModelAnimation(anims[i]); RL_FREE(anims); diff --git a/examples/models/models_material_pbr.c b/examples/models/models_material_pbr.c index 8d51eefd2..fea30cf4b 100644 --- a/examples/models/models_material_pbr.c +++ b/examples/models/models_material_pbr.c @@ -100,7 +100,9 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModel(model); // Unload skybox model + // TODO: Unload materials and shaders + + UnloadModel(model); // Unload model CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/examples/models/models_mesh_generation.c b/examples/models/models_mesh_generation.c index 6c0ae6530..eaecb2712 100644 --- a/examples/models/models_mesh_generation.c +++ b/examples/models/models_mesh_generation.c @@ -115,11 +115,12 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- + UnloadTexture(texture); // Unload texture // Unload models data (GPU VRAM) for (int i = 0; i < NUM_MODELS; i++) UnloadModel(models[i]); - CloseWindow(); // Close window and OpenGL context + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/examples/models/models_obj_viewer.c b/examples/models/models_obj_viewer.c index 3f43e1c89..1d15fdd9c 100644 --- a/examples/models/models_obj_viewer.c +++ b/examples/models/models_obj_viewer.c @@ -116,6 +116,7 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- + UnloadTexture(texture); // Unload texture UnloadModel(model); // Unload model ClearDroppedFiles(); // Clear internal buffers diff --git a/examples/models/models_skybox.c b/examples/models/models_skybox.c index bad29b96a..c2849032d 100644 --- a/examples/models/models_skybox.c +++ b/examples/models/models_skybox.c @@ -89,7 +89,10 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModel(skybox); // Unload skybox model (and textures) + UnloadShader(skybox.materials[0].shader); + UnloadTexture(skybox.materials[0].maps[MAP_CUBEMAP].texture); + + UnloadModel(skybox); // Unload skybox model CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/examples/models/models_yaw_pitch_roll.c b/examples/models/models_yaw_pitch_roll.c index 72529d892..652738113 100644 --- a/examples/models/models_yaw_pitch_roll.c +++ b/examples/models/models_yaw_pitch_roll.c @@ -169,6 +169,7 @@ int main(void) //-------------------------------------------------------------------------------------- // Unload all loaded data + UnloadTexture(model.materials[0].maps[MAP_DIFFUSE].texture); UnloadModel(model); UnloadRenderTexture(framebuffer); From c661cad692f30cba08b1fac4437b08438128ffcc Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 10:56:49 +0200 Subject: [PATCH 20/28] Review all RL_CALLOC() calls All data should be properly initialized by now --- examples/models/models_material_pbr.c | 3 +-- src/models.c | 30 +++++++++++++-------------- src/raudio.c | 2 +- src/rlgl.h | 8 +++---- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/examples/models/models_material_pbr.c b/examples/models/models_material_pbr.c index fea30cf4b..0f896f21c 100644 --- a/examples/models/models_material_pbr.c +++ b/examples/models/models_material_pbr.c @@ -100,8 +100,7 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - // TODO: Unload materials and shaders - + UnloadMaterial(model.materials[0]); // Unload material: shader and textures UnloadModel(model); // Unload model CloseWindow(); // Close window and OpenGL context diff --git a/src/models.c b/src/models.c index 711576da2..a97b8c6bc 100644 --- a/src/models.c +++ b/src/models.c @@ -829,7 +829,7 @@ Material *LoadMaterials(const char *fileName, int *materialCount) Material LoadMaterialDefault(void) { Material material = { 0 }; - material.maps = (MaterialMap *)RL_CALLOC(MAX_MATERIAL_MAPS*sizeof(MaterialMap), 1); + material.maps = (MaterialMap *)RL_CALLOC(MAX_MATERIAL_MAPS, sizeof(MaterialMap)); material.shader = GetShaderDefault(); material.maps[MAP_DIFFUSE].texture = GetTextureDefault(); // White texture (1x1 pixel) @@ -1181,7 +1181,7 @@ bool IsModelAnimationValid(Model model, ModelAnimation anim) Mesh GenMeshPoly(int sides, float radius) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); int vertexCount = sides*3; // Vertices definition @@ -1244,7 +1244,7 @@ Mesh GenMeshPoly(int sides, float radius) Mesh GenMeshPlane(float width, float length, int resX, int resZ) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); #define CUSTOM_MESH_GEN_PLANE #if defined(CUSTOM_MESH_GEN_PLANE) @@ -1347,7 +1347,7 @@ Mesh GenMeshPlane(float width, float length, int resX, int resZ) mesh.vertices = (float *)RL_MALLOC(plane->ntriangles*3*3*sizeof(float)); mesh.texcoords = (float *)RL_MALLOC(plane->ntriangles*3*2*sizeof(float)); mesh.normals = (float *)RL_MALLOC(plane->ntriangles*3*3*sizeof(float)); - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int)); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); mesh.vertexCount = plane->ntriangles*3; mesh.triangleCount = plane->ntriangles; @@ -1379,7 +1379,7 @@ Mesh GenMeshPlane(float width, float length, int resX, int resZ) Mesh GenMeshCube(float width, float height, float length) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); #define CUSTOM_MESH_GEN_CUBE #if defined(CUSTOM_MESH_GEN_CUBE) @@ -1545,7 +1545,7 @@ par_shapes_mesh* par_shapes_create_icosahedron(); // 20 sides polyhedron RLAPI Mesh GenMeshSphere(float radius, int rings, int slices) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); par_shapes_mesh *sphere = par_shapes_create_parametric_sphere(slices, rings); par_shapes_scale(sphere, radius, radius, radius); @@ -1584,7 +1584,7 @@ RLAPI Mesh GenMeshSphere(float radius, int rings, int slices) RLAPI Mesh GenMeshHemiSphere(float radius, int rings, int slices) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); par_shapes_mesh *sphere = par_shapes_create_hemisphere(slices, rings); par_shapes_scale(sphere, radius, radius, radius); @@ -1623,7 +1623,7 @@ RLAPI Mesh GenMeshHemiSphere(float radius, int rings, int slices) Mesh GenMeshCylinder(float radius, float height, int slices) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); // Instance a cylinder that sits on the Z=0 plane using the given tessellation // levels across the UV domain. Think of "slices" like a number of pizza @@ -1682,7 +1682,7 @@ Mesh GenMeshCylinder(float radius, float height, int slices) Mesh GenMeshTorus(float radius, float size, int radSeg, int sides) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); if (radius > 1.0f) radius = 1.0f; else if (radius < 0.1f) radius = 0.1f; @@ -1725,7 +1725,7 @@ Mesh GenMeshTorus(float radius, float size, int radSeg, int sides) Mesh GenMeshKnot(float radius, float size, int radSeg, int sides) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); if (radius > 3.0f) radius = 3.0f; else if (radius < 0.5f) radius = 0.5f; @@ -1769,7 +1769,7 @@ Mesh GenMeshHeightmap(Image heightmap, Vector3 size) #define GRAY_VALUE(c) ((c.r+c.g+c.b)/3) Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); int mapX = heightmap.width; int mapZ = heightmap.height; @@ -1878,7 +1878,7 @@ Mesh GenMeshHeightmap(Image heightmap, Vector3 size) Mesh GenMeshCubicmap(Image cubicmap, Vector3 cubeSize) { Mesh mesh = { 0 }; - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); Color *cubicmapPixels = GetImageData(cubicmap); @@ -2819,7 +2819,7 @@ static Model LoadOBJ(const char *fileName) mesh.vertices = (float *)RL_MALLOC(mesh.vertexCount*3*sizeof(float)); mesh.texcoords = (float *)RL_MALLOC(mesh.vertexCount*2*sizeof(float)); mesh.normals = (float *)RL_MALLOC(mesh.vertexCount*3*sizeof(float)); - mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); int vCount = 0; int vtCount = 0; @@ -3089,7 +3089,7 @@ static Model LoadIQM(const char *fileName) model.meshes[i].animVertices = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*3); model.meshes[i].animNormals = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*3); - model.meshes[i].vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + model.meshes[i].vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); } // Triangles data processing @@ -3411,7 +3411,7 @@ static Model LoadGLTF(const char *fileName) model.materials = RL_MALLOC(model.materialCount*sizeof(Material)); model.meshMaterial = RL_MALLOC(model.meshCount*sizeof(int)); - for (int i = 0; i < model.meshCount; i++) model.meshes[i].vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO*sizeof(unsigned int), 1); + for (int i = 0; i < model.meshCount; i++) model.meshes[i].vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); for (int i = 0; i < model.materialCount - 1; i++) { diff --git a/src/raudio.c b/src/raudio.c index 576190deb..43e63bc99 100644 --- a/src/raudio.c +++ b/src/raudio.c @@ -591,7 +591,7 @@ void SetMasterVolume(float volume) // Create a new audio buffer. Initially filled with silence AudioBuffer *InitAudioBuffer(ma_format format, ma_uint32 channels, ma_uint32 sampleRate, ma_uint32 bufferSizeInFrames, int usage) { - AudioBuffer *audioBuffer = (AudioBuffer *)RL_CALLOC(sizeof(*audioBuffer), 1); + AudioBuffer *audioBuffer = (AudioBuffer *)RL_CALLOC(1, sizeof(AudioBuffer)); audioBuffer->buffer = RL_CALLOC((bufferSizeInFrames*channels*ma_get_bytes_per_sample(format)), 1); if (audioBuffer == NULL) diff --git a/src/rlgl.h b/src/rlgl.h index c71033cb0..ae75a6441 100644 --- a/src/rlgl.h +++ b/src/rlgl.h @@ -1539,7 +1539,7 @@ void rlglInit(int width, int height) // NOTE: We have to duplicate string because glGetString() returns a const string int len = strlen(extensions) + 1; - char *extensionsDup = (char *)RL_CALLOC(len, 1); + char *extensionsDup = (char *)RL_CALLOC(len, sizeof(char)); strcpy(extensionsDup, extensions); extList[numExt] = extensionsDup; @@ -2967,7 +2967,7 @@ char *LoadText(const char *fileName) Shader LoadShader(const char *vsFileName, const char *fsFileName) { Shader shader = { 0 }; - shader.locs = (int *)RL_CALLOC(MAX_SHADER_LOCATIONS*sizeof(int), 1); + shader.locs = (int *)RL_CALLOC(MAX_SHADER_LOCATIONS, sizeof(int)); char *vShaderStr = NULL; char *fShaderStr = NULL; @@ -2988,7 +2988,7 @@ Shader LoadShader(const char *vsFileName, const char *fsFileName) Shader LoadShaderCode(char *vsCode, char *fsCode) { Shader shader = { 0 }; - shader.locs = (int *)RL_CALLOC(MAX_SHADER_LOCATIONS*sizeof(int), 1); + shader.locs = (int *)RL_CALLOC(MAX_SHADER_LOCATIONS, sizeof(int)); // NOTE: All locations must be reseted to -1 (no location) for (int i = 0; i < MAX_SHADER_LOCATIONS; i++) shader.locs[i] = -1; @@ -3861,7 +3861,7 @@ static unsigned int LoadShaderProgram(unsigned int vShaderId, unsigned int fShad static Shader LoadShaderDefault(void) { Shader shader = { 0 }; - shader.locs = (int *)RL_CALLOC(MAX_SHADER_LOCATIONS*sizeof(int), 1); + shader.locs = (int *)RL_CALLOC(MAX_SHADER_LOCATIONS, sizeof(int)); // NOTE: All locations must be reseted to -1 (no location) for (int i = 0; i < MAX_SHADER_LOCATIONS; i++) shader.locs[i] = -1; From 8241dcf80b1177316d377a6f5e16bdee40692c39 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 11:16:43 +0200 Subject: [PATCH 21/28] Review models loading, memory initialization --- src/models.c | 82 +++++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/src/models.c b/src/models.c index a97b8c6bc..581aa1ac0 100644 --- a/src/models.c +++ b/src/models.c @@ -686,14 +686,14 @@ Model LoadModelFromMesh(Mesh mesh) model.transform = MatrixIdentity(); model.meshCount = 1; - model.meshes = (Mesh *)RL_MALLOC(model.meshCount*sizeof(Mesh)); + model.meshes = (Mesh *)RL_CALLOC(model.meshCount, sizeof(Mesh)); model.meshes[0] = mesh; model.materialCount = 1; - model.materials = (Material *)RL_MALLOC(model.materialCount*sizeof(Material)); + model.materials = (Material *)RL_CALLOC(model.materialCount, sizeof(Material)); model.materials[0] = LoadMaterialDefault(); - model.meshMaterial = (int *)RL_MALLOC(model.meshCount*sizeof(int)); + model.meshMaterial = (int *)RL_CALLOC(model.meshCount, sizeof(int)); model.meshMaterial[0] = 0; // First material index return model; @@ -2792,11 +2792,15 @@ static Model LoadOBJ(const char *fileName) // TODO: Support multiple meshes... in the meantime, only one mesh is returned //model.meshCount = meshCount; model.meshCount = 1; - model.meshes = (Mesh *)RL_MALLOC(model.meshCount*sizeof(Mesh)); + model.meshes = (Mesh *)RL_CALLOC(model.meshCount, sizeof(Mesh)); // Init model materials array - model.materialCount = materialCount; - model.materials = (Material *)RL_MALLOC(model.materialCount*sizeof(Material)); + if (materialCount > 0) + { + model.materialCount = materialCount; + model.materials = (Material *)RL_CALLOC(model.materialCount, sizeof(Material)); + } + model.meshMaterial = (int *)RL_CALLOC(model.meshCount, sizeof(int)); /* @@ -2816,9 +2820,9 @@ static Model LoadOBJ(const char *fileName) memset(&mesh, 0, sizeof(Mesh)); mesh.vertexCount = attrib.num_faces*3; mesh.triangleCount = attrib.num_faces; - mesh.vertices = (float *)RL_MALLOC(mesh.vertexCount*3*sizeof(float)); - mesh.texcoords = (float *)RL_MALLOC(mesh.vertexCount*2*sizeof(float)); - mesh.normals = (float *)RL_MALLOC(mesh.vertexCount*3*sizeof(float)); + mesh.vertices = (float *)RL_CALLOC(mesh.vertexCount*3, sizeof(float)); + mesh.texcoords = (float *)RL_CALLOC(mesh.vertexCount*2, sizeof(float)); + mesh.normals = (float *)RL_CALLOC(mesh.vertexCount*3, sizeof(float)); mesh.vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); int vCount = 0; @@ -3066,36 +3070,36 @@ static Model LoadIQM(const char *fileName) model.meshCount = iqm.num_meshes; model.meshes = RL_CALLOC(model.meshCount, sizeof(Mesh)); - char name[MESH_NAME_LENGTH]; + char name[MESH_NAME_LENGTH] = { 0 }; for (int i = 0; i < model.meshCount; i++) { - fseek(iqmFile,iqm.ofs_text+imesh[i].name,SEEK_SET); + fseek(iqmFile, iqm.ofs_text + imesh[i].name, SEEK_SET); fread(name, sizeof(char)*MESH_NAME_LENGTH, 1, iqmFile); // Mesh name not used... model.meshes[i].vertexCount = imesh[i].num_vertexes; - model.meshes[i].vertices = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*3); // Default vertex positions - model.meshes[i].normals = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*3); // Default vertex normals - model.meshes[i].texcoords = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*2); // Default vertex texcoords + model.meshes[i].vertices = RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); // Default vertex positions + model.meshes[i].normals = RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); // Default vertex normals + model.meshes[i].texcoords = RL_CALLOC(model.meshes[i].vertexCount*2, sizeof(float)); // Default vertex texcoords - model.meshes[i].boneIds = RL_MALLOC(sizeof(int)*model.meshes[i].vertexCount*4); // Up-to 4 bones supported! - model.meshes[i].boneWeights = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*4); // Up-to 4 bones supported! + model.meshes[i].boneIds = RL_CALLOC(model.meshes[i].vertexCount*4, sizeof(float)); // Up-to 4 bones supported! + model.meshes[i].boneWeights = RL_CALLOC(model.meshes[i].vertexCount*4, sizeof(float)); // Up-to 4 bones supported! model.meshes[i].triangleCount = imesh[i].num_triangles; - model.meshes[i].indices = RL_MALLOC(sizeof(unsigned short)*model.meshes[i].triangleCount*3); + model.meshes[i].indices = RL_CALLOC(model.meshes[i].triangleCount*3, sizeof(unsigned short)); // Animated verted data, what we actually process for rendering // NOTE: Animated vertex should be re-uploaded to GPU (if not using GPU skinning) - model.meshes[i].animVertices = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*3); - model.meshes[i].animNormals = RL_MALLOC(sizeof(float)*model.meshes[i].vertexCount*3); + model.meshes[i].animVertices = RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); + model.meshes[i].animNormals = RL_CALLOC(model.meshes[i].vertexCount*3, sizeof(float)); model.meshes[i].vboId = (unsigned int *)RL_CALLOC(MAX_MESH_VBO, sizeof(unsigned int)); } // Triangles data processing - tri = RL_MALLOC(sizeof(IQMTriangle)*iqm.num_triangles); + tri = RL_MALLOC(iqm.num_triangles*sizeof(IQMTriangle)); fseek(iqmFile, iqm.ofs_triangles, SEEK_SET); - fread(tri, sizeof(IQMTriangle)*iqm.num_triangles, 1, iqmFile); + fread(tri, iqm.num_triangles*sizeof(IQMTriangle), 1, iqmFile); for (int m = 0; m < model.meshCount; m++) { @@ -3112,9 +3116,9 @@ static Model LoadIQM(const char *fileName) } // Vertex arrays data processing - va = RL_MALLOC(sizeof(IQMVertexArray)*iqm.num_vertexarrays); + va = RL_MALLOC(iqm.num_vertexarrays*sizeof(IQMVertexArray)); fseek(iqmFile, iqm.ofs_vertexarrays, SEEK_SET); - fread(va, sizeof(IQMVertexArray)*iqm.num_vertexarrays, 1, iqmFile); + fread(va, iqm.num_vertexarrays*sizeof(IQMVertexArray), 1, iqmFile); for (int i = 0; i < iqm.num_vertexarrays; i++) { @@ -3122,9 +3126,9 @@ static Model LoadIQM(const char *fileName) { case IQM_POSITION: { - vertex = RL_MALLOC(sizeof(float)*iqm.num_vertexes*3); + vertex = RL_MALLOC(iqm.num_vertexes*3*sizeof(float)); fseek(iqmFile, va[i].offset, SEEK_SET); - fread(vertex, sizeof(float)*iqm.num_vertexes*3, 1, iqmFile); + fread(vertex, iqm.num_vertexes*3*sizeof(float), 1, iqmFile); for (int m = 0; m < iqm.num_meshes; m++) { @@ -3139,9 +3143,9 @@ static Model LoadIQM(const char *fileName) } break; case IQM_NORMAL: { - normal = RL_MALLOC(sizeof(float)*iqm.num_vertexes*3); + normal = RL_MALLOC(iqm.num_vertexes*3*sizeof(float)); fseek(iqmFile, va[i].offset, SEEK_SET); - fread(normal, sizeof(float)*iqm.num_vertexes*3, 1, iqmFile); + fread(normal, iqm.num_vertexes*3*sizeof(float), 1, iqmFile); for (int m = 0; m < iqm.num_meshes; m++) { @@ -3156,9 +3160,9 @@ static Model LoadIQM(const char *fileName) } break; case IQM_TEXCOORD: { - text = RL_MALLOC(sizeof(float)*iqm.num_vertexes*2); + text = RL_MALLOC(iqm.num_vertexes*2*sizeof(float)); fseek(iqmFile, va[i].offset, SEEK_SET); - fread(text, sizeof(float)*iqm.num_vertexes*2, 1, iqmFile); + fread(text, iqm.num_vertexes*2*sizeof(float), 1, iqmFile); for (int m = 0; m < iqm.num_meshes; m++) { @@ -3172,9 +3176,9 @@ static Model LoadIQM(const char *fileName) } break; case IQM_BLENDINDEXES: { - blendi = RL_MALLOC(sizeof(char)*iqm.num_vertexes*4); + blendi = RL_MALLOC(iqm.num_vertexes*4*sizeof(char)); fseek(iqmFile, va[i].offset, SEEK_SET); - fread(blendi, sizeof(char)*iqm.num_vertexes*4, 1, iqmFile); + fread(blendi, iqm.num_vertexes*4*sizeof(char), 1, iqmFile); for (int m = 0; m < iqm.num_meshes; m++) { @@ -3188,9 +3192,9 @@ static Model LoadIQM(const char *fileName) } break; case IQM_BLENDWEIGHTS: { - blendw = RL_MALLOC(sizeof(unsigned char)*iqm.num_vertexes*4); - fseek(iqmFile,va[i].offset,SEEK_SET); - fread(blendw,sizeof(unsigned char)*iqm.num_vertexes*4,1,iqmFile); + blendw = RL_MALLOC(iqm.num_vertexes*4*sizeof(unsigned char)); + fseek(iqmFile, va[i].offset, SEEK_SET); + fread(blendw, iqm.num_vertexes*4*sizeof(unsigned char), 1, iqmFile); for (int m = 0; m < iqm.num_meshes; m++) { @@ -3206,20 +3210,20 @@ static Model LoadIQM(const char *fileName) } // Bones (joints) data processing - ijoint = RL_MALLOC(sizeof(IQMJoint)*iqm.num_joints); + ijoint = RL_MALLOC(iqm.num_joints*sizeof(IQMJoint)); fseek(iqmFile, iqm.ofs_joints, SEEK_SET); - fread(ijoint, sizeof(IQMJoint)*iqm.num_joints, 1, iqmFile); + fread(ijoint, iqm.num_joints*sizeof(IQMJoint), 1, iqmFile); model.boneCount = iqm.num_joints; - model.bones = RL_MALLOC(sizeof(BoneInfo)*iqm.num_joints); - model.bindPose = RL_MALLOC(sizeof(Transform)*iqm.num_joints); + model.bones = RL_MALLOC(iqm.num_joints*sizeof(BoneInfo)); + model.bindPose = RL_MALLOC(iqm.num_joints*sizeof(Transform)); for (int i = 0; i < iqm.num_joints; i++) { // Bones model.bones[i].parent = ijoint[i].parent; fseek(iqmFile, iqm.ofs_text + ijoint[i].name, SEEK_SET); - fread(model.bones[i].name,sizeof(char)*BONE_NAME_LENGTH, 1, iqmFile); + fread(model.bones[i].name, BONE_NAME_LENGTH*sizeof(char), 1, iqmFile); // Bind pose (base pose) model.bindPose[i].translation.x = ijoint[i].translate[0]; From f565ff018e194bb0ed4501c732ffaace117ba55a Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 11:24:04 +0200 Subject: [PATCH 22/28] REMOVED: FLAG_SHOW_LOGO raylib logo animation has been removed. That funtionality was very restrictive, not configurable at all and hardly ever used, also, it added a coupling dependency of `core` module with `shapes` and `text` modules. --- src/core.c | 132 --------------------------------------------------- src/raylib.h | 2 +- 2 files changed, 1 insertion(+), 133 deletions(-) diff --git a/src/core.c b/src/core.c index bc633e8b7..f07fcde7f 100644 --- a/src/core.c +++ b/src/core.c @@ -426,7 +426,6 @@ static double targetTime = 0.0; // Desired time for one frame, if 0 // Config internal variables //----------------------------------------------------------------------------------- static unsigned int configFlags = 0; // Configuration flags (bit based) -static bool showLogo = false; // Track if showing logo at init is enabled static char **dropFilesPath; // Store dropped files paths as strings static int dropFilesCount = 0; // Count dropped files strings @@ -469,8 +468,6 @@ static int GetGamepadButton(int button); // Get gamepad button ge static int GetGamepadAxis(int axis); // Get gamepad axis generic to all platforms static void PollInputEvents(void); // Register user events -static void LogoAnimation(void); // Plays raylib logo appearing animation - #if defined(PLATFORM_DESKTOP) || defined(PLATFORM_WEB) static void ErrorCallback(int error, const char *description); // GLFW3 Error Callback, runs on GLFW3 error static void KeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods); // GLFW3 Keyboard Callback, runs on key pressed @@ -700,13 +697,6 @@ void InitWindow(int width, int height, const char *title) mousePosition.x = (float)screenWidth/2.0f; mousePosition.y = (float)screenHeight/2.0f; - - // raylib logo appearing animation (if enabled) - if (showLogo) - { - SetTargetFPS(60); - LogoAnimation(); - } #endif // PLATFORM_ANDROID } @@ -1703,7 +1693,6 @@ void SetConfigFlags(unsigned int flags) { configFlags = flags; - if (configFlags & FLAG_SHOW_LOGO) showLogo = true; if (configFlags & FLAG_FULLSCREEN_MODE) fullscreen = true; if (configFlags & FLAG_WINDOW_ALWAYS_RUN) alwaysRun = true; } @@ -3974,13 +3963,6 @@ static void AndroidCommandCallback(struct android_app *app, int32_t cmd) } } */ - - // raylib logo appearing animation (if enabled) - if (showLogo) - { - SetTargetFPS(60); // Not required on Android - LogoAnimation(); - } } } } break; @@ -5028,117 +5010,3 @@ static void *GamepadThread(void *arg) return NULL; } #endif // PLATFORM_RPI - -// Plays raylib logo appearing animation -static void LogoAnimation(void) -{ -#if !defined(PLATFORM_WEB) && !defined(PLATFORM_UWP) - int logoPositionX = screenWidth/2 - 128; - int logoPositionY = screenHeight/2 - 128; - - int framesCounter = 0; - int lettersCount = 0; - - int topSideRecWidth = 16; - int leftSideRecHeight = 16; - - int bottomSideRecWidth = 16; - int rightSideRecHeight = 16; - - int state = 0; // Tracking animation states (State Machine) - float alpha = 1.0f; // Useful for fading - - while (!WindowShouldClose() && (state != 4)) // Detect window close button or ESC key - { - // Update - //---------------------------------------------------------------------------------- - if (state == 0) // State 0: Small box blinking - { - framesCounter++; - - if (framesCounter == 84) - { - state = 1; - framesCounter = 0; // Reset counter... will be used later... - } - } - else if (state == 1) // State 1: Top and left bars growing - { - topSideRecWidth += 4; - leftSideRecHeight += 4; - - if (topSideRecWidth == 256) state = 2; - } - else if (state == 2) // State 2: Bottom and right bars growing - { - bottomSideRecWidth += 4; - rightSideRecHeight += 4; - - if (bottomSideRecWidth == 256) state = 3; - } - else if (state == 3) // State 3: Letters appearing (one by one) - { - framesCounter++; - - if (framesCounter/12) // Every 12 frames, one more letter! - { - lettersCount++; - framesCounter = 0; - } - - if (lettersCount >= 10) // When all letters have appeared, just fade out everything - { - alpha -= 0.02f; - - if (alpha <= 0.0f) - { - alpha = 0.0f; - state = 4; - } - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - - ClearBackground(RAYWHITE); - - if (state == 0) - { - if ((framesCounter/12)%2) DrawRectangle(logoPositionX, logoPositionY, 16, 16, BLACK); - } - else if (state == 1) - { - DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, BLACK); - DrawRectangle(logoPositionX, logoPositionY, 16, leftSideRecHeight, BLACK); - } - else if (state == 2) - { - DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, BLACK); - DrawRectangle(logoPositionX, logoPositionY, 16, leftSideRecHeight, BLACK); - - DrawRectangle(logoPositionX + 240, logoPositionY, 16, rightSideRecHeight, BLACK); - DrawRectangle(logoPositionX, logoPositionY + 240, bottomSideRecWidth, 16, BLACK); - } - else if (state == 3) - { - DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, Fade(BLACK, alpha)); - DrawRectangle(logoPositionX, logoPositionY + 16, 16, leftSideRecHeight - 32, Fade(BLACK, alpha)); - - DrawRectangle(logoPositionX + 240, logoPositionY + 16, 16, rightSideRecHeight - 32, Fade(BLACK, alpha)); - DrawRectangle(logoPositionX, logoPositionY + 240, bottomSideRecWidth, 16, Fade(BLACK, alpha)); - - DrawRectangle(screenWidth/2 - 112, screenHeight/2 - 112, 224, 224, Fade(RAYWHITE, alpha)); - - DrawText(TextSubtext("raylib", 0, lettersCount), screenWidth/2 - 44, screenHeight/2 + 48, 50, Fade(BLACK, alpha)); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- - } -#endif - - showLogo = false; // Prevent for repeating when reloading window (Android) -} diff --git a/src/raylib.h b/src/raylib.h index 56b1f95ea..2e664fba8 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -460,7 +460,7 @@ typedef struct VrDeviceInfo { // System config flags // NOTE: Used for bit masks typedef enum { - FLAG_SHOW_LOGO = 1, // Set to show raylib logo at startup + FLAG_RESERVED = 1, // Reserved FLAG_FULLSCREEN_MODE = 2, // Set to run program in fullscreen FLAG_WINDOW_RESIZABLE = 4, // Set to allow resizable window FLAG_WINDOW_UNDECORATED = 8, // Set to disable window decoration (frame and buttons) From e2183f0b9968d9cbff0cce9db63dda2c5c96fb32 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 12:10:09 +0200 Subject: [PATCH 23/28] Update version in case no config.h is being used --- src/core.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.c b/src/core.c index f07fcde7f..5ec7d3028 100644 --- a/src/core.c +++ b/src/core.c @@ -108,7 +108,7 @@ #if !defined(EXTERNAL_CONFIG_FLAGS) #include "config.h" // Defines module configuration flags #else - #define RAYLIB_VERSION "2.5" + #define RAYLIB_VERSION "2.6-dev" #endif #if (defined(__linux__) || defined(PLATFORM_WEB)) && _POSIX_C_SOURCE < 199309L From 41732bebe8bd048cb44e5b6a24f8e2cbd8af7120 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 12:22:54 +0200 Subject: [PATCH 24/28] Formatting tweak --- src/core.c | 4 ++-- src/rlgl.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core.c b/src/core.c index 5ec7d3028..c025b2dea 100644 --- a/src/core.c +++ b/src/core.c @@ -4293,8 +4293,8 @@ static EM_BOOL EmscriptenGamepadCallback(int eventType, const EmscriptenGamepadE eventType != 0? emscripten_event_type_to_string(eventType) : "Gamepad state", gamepadEvent->timestamp, gamepadEvent->connected, gamepadEvent->index, gamepadEvent->numAxes, gamepadEvent->numButtons, gamepadEvent->id, gamepadEvent->mapping); - for(int i = 0; i < gamepadEvent->numAxes; ++i) TraceLog(LOG_DEBUG, "Axis %d: %g", i, gamepadEvent->axis[i]); - for(int i = 0; i < gamepadEvent->numButtons; ++i) TraceLog(LOG_DEBUG, "Button %d: Digital: %d, Analog: %g", i, gamepadEvent->digitalButton[i], gamepadEvent->analogButton[i]); + for (int i = 0; i < gamepadEvent->numAxes; ++i) TraceLog(LOG_DEBUG, "Axis %d: %g", i, gamepadEvent->axis[i]); + for (int i = 0; i < gamepadEvent->numButtons; ++i) TraceLog(LOG_DEBUG, "Button %d: Digital: %d, Analog: %g", i, gamepadEvent->digitalButton[i], gamepadEvent->analogButton[i]); */ if ((gamepadEvent->connected) && (gamepadEvent->index < MAX_GAMEPADS)) gamepadReady[gamepadEvent->index] = true; diff --git a/src/rlgl.h b/src/rlgl.h index ae75a6441..85d60fe19 100644 --- a/src/rlgl.h +++ b/src/rlgl.h @@ -3024,7 +3024,7 @@ Shader LoadShaderCode(char *vsCode, char *fsCode) glGetProgramiv(shader.id, GL_ACTIVE_UNIFORMS, &uniformCount); - for(int i = 0; i < uniformCount; i++) + for (int i = 0; i < uniformCount; i++) { int namelen = -1; int num = -1; From 37bb8e9554d49030f888092b9615aff08b07438f Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 12:23:30 +0200 Subject: [PATCH 25/28] UpdateModelAnimation() - Added security check --- src/models.c | 128 +++++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/src/models.c b/src/models.c index 581aa1ac0..b3a50f403 100644 --- a/src/models.c +++ b/src/models.c @@ -919,7 +919,7 @@ ModelAnimation *LoadModelAnimations(const char *filename, int *animCount) TraceLog(LOG_ERROR, "[%s] Unable to open file", filename); } - // header + // Read IQM header fread(&iqm, sizeof(IQMHeader), 1, iqmFile); if (strncmp(iqm.magic, IQM_MAGIC, sizeof(IQM_MAGIC))) @@ -934,34 +934,30 @@ ModelAnimation *LoadModelAnimations(const char *filename, int *animCount) fclose(iqmFile); } - // bones - IQMPose *poses; - poses = RL_MALLOC(sizeof(IQMPose)*iqm.num_poses); + // Get bones data + IQMPose *poses = RL_MALLOC(iqm.num_poses*sizeof(IQMPose)); fseek(iqmFile, iqm.ofs_poses, SEEK_SET); - fread(poses, sizeof(IQMPose)*iqm.num_poses, 1, iqmFile); + fread(poses, iqm.num_poses*sizeof(IQMPose), 1, iqmFile); - // animations + // Get animations data *animCount = iqm.num_anims; IQMAnim *anim = RL_MALLOC(iqm.num_anims*sizeof(IQMAnim)); fseek(iqmFile, iqm.ofs_anims, SEEK_SET); fread(anim, iqm.num_anims*sizeof(IQMAnim), 1, iqmFile); ModelAnimation *animations = RL_MALLOC(iqm.num_anims*sizeof(ModelAnimation)); - // frameposes - unsigned short *framedata = RL_MALLOC(sizeof(unsigned short)*iqm.num_frames*iqm.num_framechannels); + unsigned short *framedata = RL_MALLOC(iqm.num_frames*iqm.num_framechannels*sizeof(unsigned short)); fseek(iqmFile, iqm.ofs_frames, SEEK_SET); - fread(framedata, sizeof(unsigned short)*iqm.num_frames*iqm.num_framechannels, 1, iqmFile); + fread(framedata, iqm.num_frames*iqm.num_framechannels*sizeof(unsigned short), 1, iqmFile); - for(int a=0;a= anim.frameCount) frame = frame%anim.frameCount; - - for (int m = 0; m < model.meshCount; m++) + if ((anim.frameCount > 0) && (anim.bones != NULL) && (anim.framePoses != NULL)) { - Vector3 animVertex = { 0 }; - Vector3 animNormal = { 0 }; + if (frame >= anim.frameCount) frame = frame%anim.frameCount; - Vector3 inTranslation = { 0 }; - Quaternion inRotation = { 0 }; - Vector3 inScale = { 0 }; - - Vector3 outTranslation = { 0 }; - Quaternion outRotation = { 0 }; - Vector3 outScale = { 0 }; - - int vCounter = 0; - int boneCounter = 0; - int boneId = 0; - - for (int i = 0; i < model.meshes[m].vertexCount; i++) + for (int m = 0; m < model.meshCount; m++) { - boneId = model.meshes[m].boneIds[boneCounter]; - inTranslation = model.bindPose[boneId].translation; - inRotation = model.bindPose[boneId].rotation; - inScale = model.bindPose[boneId].scale; - outTranslation = anim.framePoses[frame][boneId].translation; - outRotation = anim.framePoses[frame][boneId].rotation; - outScale = anim.framePoses[frame][boneId].scale; + Vector3 animVertex = { 0 }; + Vector3 animNormal = { 0 }; - // Vertices processing - // NOTE: We use meshes.vertices (default vertex position) to calculate meshes.animVertices (animated vertex position) - animVertex = (Vector3){ model.meshes[m].vertices[vCounter], model.meshes[m].vertices[vCounter + 1], model.meshes[m].vertices[vCounter + 2] }; - animVertex = Vector3MultiplyV(animVertex, outScale); - animVertex = Vector3Subtract(animVertex, inTranslation); - animVertex = Vector3RotateByQuaternion(animVertex, QuaternionMultiply(outRotation, QuaternionInvert(inRotation))); - animVertex = Vector3Add(animVertex, outTranslation); - model.meshes[m].animVertices[vCounter] = animVertex.x; - model.meshes[m].animVertices[vCounter + 1] = animVertex.y; - model.meshes[m].animVertices[vCounter + 2] = animVertex.z; + Vector3 inTranslation = { 0 }; + Quaternion inRotation = { 0 }; + Vector3 inScale = { 0 }; - // Normals processing - // NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals) - animNormal = (Vector3){ model.meshes[m].normals[vCounter], model.meshes[m].normals[vCounter + 1], model.meshes[m].normals[vCounter + 2] }; - animNormal = Vector3RotateByQuaternion(animNormal, QuaternionMultiply(outRotation, QuaternionInvert(inRotation))); - model.meshes[m].animNormals[vCounter] = animNormal.x; - model.meshes[m].animNormals[vCounter + 1] = animNormal.y; - model.meshes[m].animNormals[vCounter + 2] = animNormal.z; - vCounter += 3; + Vector3 outTranslation = { 0 }; + Quaternion outRotation = { 0 }; + Vector3 outScale = { 0 }; - boneCounter += 4; + int vCounter = 0; + int boneCounter = 0; + int boneId = 0; + + for (int i = 0; i < model.meshes[m].vertexCount; i++) + { + boneId = model.meshes[m].boneIds[boneCounter]; + inTranslation = model.bindPose[boneId].translation; + inRotation = model.bindPose[boneId].rotation; + inScale = model.bindPose[boneId].scale; + outTranslation = anim.framePoses[frame][boneId].translation; + outRotation = anim.framePoses[frame][boneId].rotation; + outScale = anim.framePoses[frame][boneId].scale; + + // Vertices processing + // NOTE: We use meshes.vertices (default vertex position) to calculate meshes.animVertices (animated vertex position) + animVertex = (Vector3){ model.meshes[m].vertices[vCounter], model.meshes[m].vertices[vCounter + 1], model.meshes[m].vertices[vCounter + 2] }; + animVertex = Vector3MultiplyV(animVertex, outScale); + animVertex = Vector3Subtract(animVertex, inTranslation); + animVertex = Vector3RotateByQuaternion(animVertex, QuaternionMultiply(outRotation, QuaternionInvert(inRotation))); + animVertex = Vector3Add(animVertex, outTranslation); + model.meshes[m].animVertices[vCounter] = animVertex.x; + model.meshes[m].animVertices[vCounter + 1] = animVertex.y; + model.meshes[m].animVertices[vCounter + 2] = animVertex.z; + + // Normals processing + // NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals) + animNormal = (Vector3){ model.meshes[m].normals[vCounter], model.meshes[m].normals[vCounter + 1], model.meshes[m].normals[vCounter + 2] }; + animNormal = Vector3RotateByQuaternion(animNormal, QuaternionMultiply(outRotation, QuaternionInvert(inRotation))); + model.meshes[m].animNormals[vCounter] = animNormal.x; + model.meshes[m].animNormals[vCounter + 1] = animNormal.y; + model.meshes[m].animNormals[vCounter + 2] = animNormal.z; + vCounter += 3; + + boneCounter += 4; + } + + // Upload new vertex data to GPU for model drawing + rlUpdateBuffer(model.meshes[m].vboId[0], model.meshes[m].animVertices, model.meshes[m].vertexCount*3*sizeof(float)); // Update vertex position + rlUpdateBuffer(model.meshes[m].vboId[2], model.meshes[m].animVertices, model.meshes[m].vertexCount*3*sizeof(float)); // Update vertex normals } - - // Upload new vertex data to GPU for model drawing - rlUpdateBuffer(model.meshes[m].vboId[0], model.meshes[m].animVertices, model.meshes[m].vertexCount*3*sizeof(float)); // Update vertex position - rlUpdateBuffer(model.meshes[m].vboId[2], model.meshes[m].animVertices, model.meshes[m].vertexCount*3*sizeof(float)); // Update vertex normals } } From 97101d1003b182af354530c1447a1ee67af47e8c Mon Sep 17 00:00:00 2001 From: arvyy Date: Tue, 27 Aug 2019 14:05:28 +0300 Subject: [PATCH 26/28] Add screen->world and world->screen functions for 2D; add extended camera2D example (#947) --- examples/core/core_2d_camera_ext.c | 284 +++++++++++++++++++++++++++++ src/core.c | 59 +++--- src/raylib.h | 3 + 3 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 examples/core/core_2d_camera_ext.c diff --git a/examples/core/core_2d_camera_ext.c b/examples/core/core_2d_camera_ext.c new file mode 100644 index 000000000..330e39ef9 --- /dev/null +++ b/examples/core/core_2d_camera_ext.c @@ -0,0 +1,284 @@ +/******************************************************************************************* +* +* raylib [core] example - 2d camera extended +* +* This example has been created using raylib 1.5 (www.raylib.com) +* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* +* Copyright (c) 2016 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" +#include "raymath.h" + +#define G 400 +#define PLAYER_JUMP_SPD 350.f +#define PLAYER_HOR_SPD 200.f + +typedef struct Player { + Vector2 pos; + float vel; + int canJump; +} Player; + +typedef struct EnvItem { + Rectangle rect; + int blocking; + Color color; +} EnvItem; + +void updateCameraCenter( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + camera->offset = (Vector2){ width/2, height/2 }; + camera->target = player->pos; +} + +void updateCameraCenterInsideMap( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + camera->target = player->pos; + camera->offset = (Vector2){ width/2, height/2 }; + float minX = 1000, minY = 1000, maxX = -1000, maxY = -1000; + for (int i = 0; i < envItemsLength; i++) { + EnvItem *ei = envItems + i; + minX = fminf(ei->rect.x, minX); + maxX = fmaxf(ei->rect.x + ei->rect.width, maxX); + minY = fminf(ei->rect.y, minY); + maxY = fmaxf(ei->rect.y + ei->rect.height, maxY); + } + Vector2 max = GetWorldToScreen2D((Vector2){ maxX, maxY }, *camera); + Vector2 min = GetWorldToScreen2D((Vector2){ minX, minY }, *camera); + if (max.x < width) { + camera->offset.x = width - (max.x - width/2); + } + if (max.y < height) { + camera->offset.y = height - (max.y - height/2); + } + if (min.x > 0) { + camera->offset.x = width/2 - min.x; + } + if (min.y > 0) { + camera->offset.y = height/2- min.y; + } +} + +void updateCameraCenterSmoothFollow( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + static float minSpeed = 30; + static float minEffectLength = 10; + static float fractionSpeed = 0.8f; + camera->offset = (Vector2){ width/2, height/2 }; + Vector2 diff = Vector2Subtract(player->pos, camera->target); + float length = Vector2Length(diff); + if (length > minEffectLength) { + float speed = fmaxf(fractionSpeed * length, minSpeed); + camera->target = Vector2Add(camera->target, Vector2Scale(diff, speed*delta/length)); + } +} + +void updateCameraEvenOutOnLanding( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + static float evenOutSpeed = 700; + static int eveningOut = false; + static float evenOutTarget; + camera->offset = (Vector2){ width/2, height/2 }; + camera->target.x = player->pos.x; + if (eveningOut) { + if (evenOutTarget > camera->target.y) { + camera->target.y += evenOutSpeed * delta; + if (camera->target.y > evenOutTarget) { + camera->target.y = evenOutTarget; + eveningOut = 0; + } + } else { + camera->target.y -= evenOutSpeed * delta; + if (camera->target.y < evenOutTarget) { + camera->target.y = evenOutTarget; + eveningOut = 0; + } + } + } else { + if (player->canJump && + player->vel == 0 && + player->pos.y != camera->target.y + ) { + eveningOut = 1; + evenOutTarget = player->pos.y; + } + } +} + +void updateCameraPlayerBoundsPush( + float delta, + Camera2D *camera, + Player *player, + EnvItem *envItems, + int envItemsLength, + int width, int height +) { + static Vector2 bbox = { 0.2f, 0.2f }; + + Vector2 bboxWorldMin = GetScreenToWorld2D((Vector2){ (1 - bbox.x) * 0.5 * width, (1 - bbox.y) * 0.5 * height }, *camera); + Vector2 bboxWorldMax = GetScreenToWorld2D((Vector2){ (1 + bbox.x) * 0.5 * width, (1 + bbox.y) * 0.5 * height }, *camera); + camera->offset = (Vector2){ (1 - bbox.x) * 0.5 * width, (1 - bbox.y) * 0.5 * height }; + + if (player->pos.x < bboxWorldMin.x) { + camera->target.x = player->pos.x; + } + if (player->pos.y < bboxWorldMin.y) { + camera->target.y = player->pos.y; + } + if (player->pos.x > bboxWorldMax.x) { + camera->target.x = bboxWorldMin.x + (player->pos.x - bboxWorldMax.x); + } + if (player->pos.y > bboxWorldMax.y) { + camera->target.y = bboxWorldMin.y + (player->pos.y - bboxWorldMax.y); + } +} + + +void updatePlayer(float delta, Player *player, EnvItem *envItems, int envItemsLength) { + if (IsKeyDown(KEY_LEFT)) player->pos.x -= PLAYER_HOR_SPD*delta; + if (IsKeyDown(KEY_RIGHT)) player->pos.x += PLAYER_HOR_SPD*delta; + if (IsKeyDown(KEY_SPACE) && player->canJump) { + player->vel = -PLAYER_JUMP_SPD; + player->canJump = 0; + } + + int hitObstacle = 0; + for (int i = 0; i < envItemsLength; i++) { + EnvItem *ei = envItems + i; + Vector2 *p = &(player->pos); + if (ei->blocking && + ei->rect.x <= p->x && + ei->rect.x + ei->rect.width >= p->x && + ei->rect.y >= p->y && + ei->rect.y < p->y + player->vel * delta) + { + hitObstacle = 1; + player->vel = 0.0f; + p->y = ei->rect.y; + } + } + if (!hitObstacle) { + player->pos.y += player->vel * delta; + player->vel += G * delta; + player->canJump = 0; + } else { + player->canJump = 1; + } +} + +void renderWorld(Player *player, EnvItem *envItems, int envItemsLength) { + for (int i = 0; i < envItemsLength; i++) { + DrawRectangleRec(envItems[i].rect, envItems[i].color); + } + Rectangle playerRect = { player->pos.x - 20, player->pos.y - 40, 40, 40 }; + DrawRectangleRec(playerRect, RED); +} + +int main(void) +{ + const int screenWidth = 800; + const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); + SetTargetFPS(60); + + Player player; + player.pos = (Vector2){ 400, 280 }; + player.vel = 0; + player.canJump = 0; + EnvItem envItems[] = { + {{ 0, 0, 1000, 400 }, 0, LIGHTGRAY }, + {{ 0, 400, 1000, 200 }, 1, GRAY }, + {{ 300, 200, 400, 10 }, 1, GRAY }, + {{ 250, 300, 100, 10 }, 1, GRAY }, + {{ 650, 300, 100, 10 }, 1, GRAY } + }; + int envItemsLength = sizeof(envItems) / sizeof (envItems[0]); + + Camera2D camera = { 0 }; + camera.target = player.pos; + camera.offset = (Vector2){ screenWidth/2, screenHeight/2 }; + camera.rotation = 0.0f; + camera.zoom = 1.0f; + + int cameraOption = 0; + void (*cameraUpdaters[])(float, Camera2D*, Player*, EnvItem*, int, int, int) = { + updateCameraCenter, + updateCameraCenterInsideMap, + updateCameraCenterSmoothFollow, + updateCameraEvenOutOnLanding, + updateCameraPlayerBoundsPush + }; + int cameraUpdatersLength = sizeof(cameraUpdaters) / sizeof(cameraUpdaters[0]); + char* cameraDescriptions[] = { + "Follow player center", + "Follow player center, but clamp to map edges", + "Follow player center; smoothed", + "Follow player center horizontally; updateplayer center vertically after landing", + "Player push camera on getting too close to screen edge" + }; + + while (!WindowShouldClose()) { + float delta = GetFrameTime(); + updatePlayer(delta, &player, envItems, envItemsLength); + + camera.zoom += ((float)GetMouseWheelMove()*0.05f); + if (camera.zoom > 3.0f) camera.zoom = 3.0f; + else if (camera.zoom < 0.25f) camera.zoom = 0.25f; + if (IsKeyPressed(KEY_R)) + { + camera.zoom = 1.0f; + } + + if (IsKeyPressed(KEY_C)) { + cameraOption = (cameraOption + 1) % cameraUpdatersLength; + } + cameraUpdaters[cameraOption](delta, &camera, &player, envItems, envItemsLength, screenWidth, screenHeight); + + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode2D(camera); + renderWorld(&player, envItems, envItemsLength); + EndMode2D(); + + DrawText("Controls:", 20, 20, 10, BLACK); + DrawText("- Right/Left to move", 40, 40, 10, DARKGRAY); + DrawText("- Space to jump", 40, 60, 10, DARKGRAY); + DrawText("- Mouse Wheel to Zoom in-out, R to reset zoom", 40, 80, 10, DARKGRAY); + DrawText("- C to change camera mode", 40, 100, 10, DARKGRAY); + DrawText("Current camera mode:", 20, 120, 10, BLACK); + DrawText(cameraDescriptions[cameraOption], 40, 140, 10, DARKGRAY); + EndDrawing(); + } + + CloseWindow(); // Close window and OpenGL context + + return 0; +} diff --git a/src/core.c b/src/core.c index c025b2dea..987f6e678 100644 --- a/src/core.c +++ b/src/core.c @@ -1250,28 +1250,7 @@ void BeginMode2D(Camera2D camera) rlLoadIdentity(); // Reset current matrix (MODELVIEW) rlMultMatrixf(MatrixToFloat(screenScaling)); // Apply screen scaling if required - // The camera in world-space is set by - // 1. Move it to target - // 2. Rotate by -rotation and scale by (1/zoom) - // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), - // not for the camera getting bigger, hence the invert. Same deal with rotation. - // 3. Move it by (-offset); - // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) - // we need to do it into opposite direction (inverse transform) - - // Having camera transform in world-space, inverse of it gives the modelview transform. - // Since (A*B*C)' = C'*B'*A', the modelview is - // 1. Move to offset - // 2. Rotate and Scale - // 3. Move by -target - Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); - Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); - Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); - Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); - - Matrix matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); - - rlMultMatrixf(MatrixToFloat(matTransform)); // Apply transformation to modelview + rlMultMatrixf(MatrixToFloat(GetCamera2DMatrix(camera))); // Apply transformation to modelview } // Ends 2D mode with custom camera @@ -1502,6 +1481,42 @@ Matrix GetCameraMatrix(Camera camera) return MatrixLookAt(camera.position, camera.target, camera.up); } + +Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera) { + Matrix m = MatrixInvert(GetCamera2DMatrix(camera)); + Vector3 transform = Vector3Transform((Vector3){position.x, position.y, 0}, m); + return (Vector2){transform.x, transform.y}; +} + +Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera) { + Matrix m = GetCamera2DMatrix(camera); + Vector3 transform = Vector3Transform((Vector3){position.x, position.y, 0}, m); + return (Vector2){transform.x, transform.y}; +} + +Matrix GetCamera2DMatrix(Camera2D camera) { + // The camera in world-space is set by + // 1. Move it to target + // 2. Rotate by -rotation and scale by (1/zoom) + // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), + // not for the camera getting bigger, hence the invert. Same deal with rotation. + // 3. Move it by (-offset); + // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) + // we need to do it into opposite direction (inverse transform) + + // Having camera transform in world-space, inverse of it gives the modelview transform. + // Since (A*B*C)' = C'*B'*A', the modelview is + // 1. Move to offset + // 2. Rotate and Scale + // 3. Move by -target + Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); + Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); + Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); + Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); + Matrix matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); + return matTransform; +} + // Set target FPS (maximum) void SetTargetFPS(int fps) { diff --git a/src/raylib.h b/src/raylib.h index 2e664fba8..4dcf78ac8 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -911,6 +911,9 @@ RLAPI void EndScissorMode(void); // End scissor RLAPI Ray GetMouseRay(Vector2 mousePosition, Camera camera); // Returns a ray trace from mouse position RLAPI Vector2 GetWorldToScreen(Vector3 position, Camera camera); // Returns the screen space position for a 3d world space position RLAPI Matrix GetCameraMatrix(Camera camera); // Returns camera transform matrix (view matrix) +RLAPI Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera); +RLAPI Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera); +RLAPI Matrix GetCamera2DMatrix(Camera2D camera); // Timing-related functions RLAPI void SetTargetFPS(int fps); // Set target FPS (maximum) From 4e43192561f167b7b5c4fb92998c656b4a432a79 Mon Sep 17 00:00:00 2001 From: raysan5 Date: Tue, 27 Aug 2019 13:15:56 +0200 Subject: [PATCH 27/28] Review latest PR --- src/core.c | 93 ++++++++++++++++++++++++++++++---------------------- src/raylib.h | 8 ++--- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/core.c b/src/core.c index 987f6e678..9e26a6399 100644 --- a/src/core.c +++ b/src/core.c @@ -1248,9 +1248,12 @@ void BeginMode2D(Camera2D camera) rlglDraw(); // Draw Buffers (Only OpenGL 3+ and ES2) rlLoadIdentity(); // Reset current matrix (MODELVIEW) - rlMultMatrixf(MatrixToFloat(screenScaling)); // Apply screen scaling if required - - rlMultMatrixf(MatrixToFloat(GetCamera2DMatrix(camera))); // Apply transformation to modelview + + // Apply screen scaling if required + rlMultMatrixf(MatrixToFloat(screenScaling)); + + // Apply 2d camera transformation to modelview + rlMultMatrixf(MatrixToFloat(GetCameraMatrix2D(camera))); } // Ends 2D mode with custom camera @@ -1433,6 +1436,40 @@ Ray GetMouseRay(Vector2 mousePosition, Camera camera) return ray; } +// Get transform matrix for camera +Matrix GetCameraMatrix(Camera camera) +{ + return MatrixLookAt(camera.position, camera.target, camera.up); +} + +// Returns camera 2d transform matrix +Matrix GetCameraMatrix2D(Camera2D camera) +{ + Matrix matTransform = { 0 }; + // The camera in world-space is set by + // 1. Move it to target + // 2. Rotate by -rotation and scale by (1/zoom) + // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), + // not for the camera getting bigger, hence the invert. Same deal with rotation. + // 3. Move it by (-offset); + // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) + // we need to do it into opposite direction (inverse transform) + + // Having camera transform in world-space, inverse of it gives the modelview transform. + // Since (A*B*C)' = C'*B'*A', the modelview is + // 1. Move to offset + // 2. Rotate and Scale + // 3. Move by -target + Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); + Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); + Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); + Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); + + matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); + + return matTransform; +} + // Returns the screen space position from a 3d world space position Vector2 GetWorldToScreen(Vector3 position, Camera camera) { @@ -1475,46 +1512,22 @@ Vector2 GetWorldToScreen(Vector3 position, Camera camera) return screenPosition; } -// Get transform matrix for camera -Matrix GetCameraMatrix(Camera camera) +// Returns the screen space position for a 2d camera world space position +Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera) { - return MatrixLookAt(camera.position, camera.target, camera.up); + Matrix matCamera = GetCameraMatrix2D(camera); + Vector3 transform = Vector3Transform((Vector3){ position.x, position.y, 0 }, matCamera); + + return (Vector2){ transform.x, transform.y }; } - -Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera) { - Matrix m = MatrixInvert(GetCamera2DMatrix(camera)); - Vector3 transform = Vector3Transform((Vector3){position.x, position.y, 0}, m); - return (Vector2){transform.x, transform.y}; -} - -Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera) { - Matrix m = GetCamera2DMatrix(camera); - Vector3 transform = Vector3Transform((Vector3){position.x, position.y, 0}, m); - return (Vector2){transform.x, transform.y}; -} - -Matrix GetCamera2DMatrix(Camera2D camera) { - // The camera in world-space is set by - // 1. Move it to target - // 2. Rotate by -rotation and scale by (1/zoom) - // When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller), - // not for the camera getting bigger, hence the invert. Same deal with rotation. - // 3. Move it by (-offset); - // Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera) - // we need to do it into opposite direction (inverse transform) - - // Having camera transform in world-space, inverse of it gives the modelview transform. - // Since (A*B*C)' = C'*B'*A', the modelview is - // 1. Move to offset - // 2. Rotate and Scale - // 3. Move by -target - Matrix matOrigin = MatrixTranslate(-camera.target.x, -camera.target.y, 0.0f); - Matrix matRotation = MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, camera.rotation*DEG2RAD); - Matrix matScale = MatrixScale(camera.zoom, camera.zoom, 1.0f); - Matrix matTranslation = MatrixTranslate(camera.offset.x, camera.offset.y, 0.0f); - Matrix matTransform = MatrixMultiply(MatrixMultiply(matOrigin, MatrixMultiply(matScale, matRotation)), matTranslation); - return matTransform; +// Returns the world space position for a 2d camera screen space position +Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera) +{ + Matrix invMatCamera = MatrixInvert(GetCameraMatrix2D(camera)); + Vector3 transform = Vector3Transform((Vector3){ position.x, position.y, 0 }, invMatCamera); + + return (Vector2){ transform.x, transform.y }; } // Set target FPS (maximum) diff --git a/src/raylib.h b/src/raylib.h index 4dcf78ac8..ec6fd8a11 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -909,11 +909,11 @@ RLAPI void EndScissorMode(void); // End scissor // Screen-space-related functions RLAPI Ray GetMouseRay(Vector2 mousePosition, Camera camera); // Returns a ray trace from mouse position -RLAPI Vector2 GetWorldToScreen(Vector3 position, Camera camera); // Returns the screen space position for a 3d world space position RLAPI Matrix GetCameraMatrix(Camera camera); // Returns camera transform matrix (view matrix) -RLAPI Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera); -RLAPI Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera); -RLAPI Matrix GetCamera2DMatrix(Camera2D camera); +RLAPI Matrix GetCameraMatrix2D(Camera2D camera); // Returns camera 2d transform matrix +RLAPI Vector2 GetWorldToScreen(Vector3 position, Camera camera); // Returns the screen space position for a 3d world space position +RLAPI Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera); // Returns the screen space position for a 2d camera world space position +RLAPI Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera); // Returns the world space position for a 2d camera screen space position // Timing-related functions RLAPI void SetTargetFPS(int fps); // Set target FPS (maximum) From dde1e0671ffd71582e3ec2b1e09bcd422d829591 Mon Sep 17 00:00:00 2001 From: chriscamacho Date: Tue, 27 Aug 2019 18:58:49 +0100 Subject: [PATCH 28/28] examples makefile not deleting examples in Linux (#955) I'm not sure why the clean command is so complex (especially given that find -type f -executable | xargs rm -fv works!) but I fixed the version as is, to support x-pie-executable which was preventing it deleting anything on my system... --- examples/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Makefile b/examples/Makefile index f57888ace..ce02d55fa 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -471,7 +471,7 @@ ifeq ($(PLATFORM),PLATFORM_DESKTOP) del *.o *.exe /s endif ifeq ($(PLATFORM_OS),LINUX) - find -type f -executable | xargs file -i | grep -E 'x-object|x-archive|x-sharedlib|x-executable' | rev | cut -d ':' -f 2- | rev | xargs rm -fv + find -type f -executable | xargs file -i | grep -E 'x-object|x-archive|x-sharedlib|x-executable|x-pie-executable' | rev | cut -d ':' -f 2- | rev | xargs rm -fv endif ifeq ($(PLATFORM_OS),OSX) find . -type f -perm +ugo+x -delete