From a58dbb9081a08b01931badd953ed1fe6cd5bd1c7 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 11 Feb 2023 17:56:56 -0800 Subject: Fix IBL cubemap coordinate system and put position-normal-tangent in world space in CookTorrance. --- gfx/shaders/cook_torrance.frag | 10 +++++---- gfx/shaders/cook_torrance.vert | 12 ++++++----- gfx/shaders/cubemap_filtering.vert | 27 ++++++++++++++---------- gfx/shaders/irradiance_map.frag | 9 ++++++-- gfx/shaders/skyquad.frag | 2 ++ gfx/shaders/skyquad.vert | 17 ++++++++------- gfx/src/renderer/renderer.c | 10 +++++++-- gfx/src/util/ibl.c | 23 +++++++++++++++++---- gfx/src/util/texture.c | 42 ++++++++++++++++++++++++++++++++++++++ gltfview/src/game.c | 15 +++++++++++--- 10 files changed, 127 insertions(+), 40 deletions(-) diff --git a/gfx/shaders/cook_torrance.frag b/gfx/shaders/cook_torrance.frag index 1adb4ae..1975491 100644 --- a/gfx/shaders/cook_torrance.frag +++ b/gfx/shaders/cook_torrance.frag @@ -30,7 +30,7 @@ uniform float MaxReflectionLOD; uniform vec3 CameraPosition; // World space. -// World-space position and normal. +// World-space position, normal and tangent. in vec3 Position; #ifdef HAS_NORMALS in vec3 Normal; @@ -53,13 +53,14 @@ layout (location = 0) out vec4 Colour; /// |normalMapSample| is the normal map sample, not necessarily normalized. /// /// TODO: Move to "normal.h" +#if defined(HAS_NORMAL_MAP) && (defined(HAS_TANGENTS) || defined(HAS_TEXCOORDS)) vec3 get_ws_normal(vec3 normalWs, vec3 normalMapSample) { vec3 N = normalize(Normal); #ifdef HAS_TANGENTS //vec3 T = normalize(tangent.xyz - dot(tangent.xyz, N) * N); vec3 T = Tangent.xyz; vec3 B = Tangent.w * cross(N, T); -#else // No tangents +#elif HAS_TEXCOORDS // No tangents, but must have texcoords. vec3 pos_dx = dFdx(Position); vec3 pos_dy = dFdy(Position); // vec3 uv_dx = vec3(dFdx(Texcoord), 0.0); @@ -71,7 +72,7 @@ vec3 get_ws_normal(vec3 normalWs, vec3 normalMapSample) { // vec3 T = pos_dx * uv_dy.t - pos_dy * uv_dx.t; T = normalize(T - dot(T, N) * N); vec3 B = normalize(cross(N, T)); -#endif // HAS_TANGENTS +#endif if (gl_FrontFacing == false) { T = -T; @@ -83,6 +84,7 @@ vec3 get_ws_normal(vec3 normalWs, vec3 normalMapSample) { //return normalize(s.x * T + s.y * B + s.z * N); return normalize(mat3(T,B,N) * s); } +#endif // HAS_TANGENTS || HAS_TEXCOORDS float trowbridge_reitz_GGX(float roughness, float NdotH) { float a = roughness * roughness; @@ -188,7 +190,7 @@ void main() #else float occlusion = 1.0; #endif - float metallic = metal_roughness.x; + float metallic = metal_roughness.x; float roughness = metal_roughness.y; // For a single light direction: diff --git a/gfx/shaders/cook_torrance.vert b/gfx/shaders/cook_torrance.vert index 697bb0c..5f126c0 100644 --- a/gfx/shaders/cook_torrance.vert +++ b/gfx/shaders/cook_torrance.vert @@ -1,7 +1,8 @@ precision highp float; uniform mat4 ModelMatrix; -uniform mat4 Modelview; +// uniform mat4 Modelview; +uniform mat4 View; uniform mat4 Projection; //uniform mat4 MVP; #ifdef HAS_JOINTS @@ -34,7 +35,7 @@ layout (location = 4) in uvec4 vJoint; layout (location = 5) in vec4 vWeight; #endif -// World-space position and normal. +// World-space position, normal and tangent. out vec3 Position; #ifdef HAS_NORMALS out vec3 Normal; @@ -54,9 +55,9 @@ void main() vWeight.y * JointMatrices[vJoint.y] + vWeight.z * JointMatrices[vJoint.z] + vWeight.w * JointMatrices[vJoint.w]; - Position = vec3(Modelview * skinMatrix * vec4(vPosition, 1.0)); + Position = vec3(ModelMatrix * skinMatrix * vec4(vPosition, 1.0)); #else - Position = vec3(Modelview * vec4(vPosition, 1.0)); + Position = vec3(ModelMatrix * vec4(vPosition, 1.0)); #endif #ifdef HAS_NORMALS Normal = mat3(ModelMatrix) * vNormal; @@ -68,6 +69,7 @@ void main() #ifdef HAS_TEXCOORDS Texcoord = vTexcoord; #endif - gl_Position = Projection * vec4(Position, 1.0); + gl_Position = Projection * View * vec4(Position, 1.0); + //gl_Position = Projection * vec4(Position, 1.0); //gl_Position = MVP * vec4(vPosition, 1.0); } diff --git a/gfx/shaders/cubemap_filtering.vert b/gfx/shaders/cubemap_filtering.vert index cdc291c..d0cf73f 100644 --- a/gfx/shaders/cubemap_filtering.vert +++ b/gfx/shaders/cubemap_filtering.vert @@ -3,32 +3,37 @@ precision highp float; #define PI 3.1415926535897932384626433832795 #define FOVY (90.0 * PI / 180.0) -uniform mat4 Modelview; +uniform mat4 CameraRotation; // From camera space to world space. +uniform float Flip; layout (location = 0) in vec2 vPosition; out vec3 Ray; +// DEBUG +// out vec2 Texcoord; + // This is very similar to the skyquad vertex shader. // // The ray is not normalized because it isn't necessary for cubemap sampling. // // We also use a fixed fovy = 90 degrees because we want the frustum to pass // exactly through each face of the cube. The aspect ratio is also just 1. -// -// |d| here appears with a positive sign in the returned vector because the -// rotation part of the Modelview matrix is responsible for orienting the ray -// appropriately. -vec3 sky_ray(vec2 Texcoord) +vec3 sky_ray(vec2 FilmPosition) { float d = 0.5 / tan(FOVY/2.0); - return vec3(Texcoord.x - 0.5, Texcoord.y - 0.5, d); + return vec3(FilmPosition, -d); } void main() { - mat3 Rotation = mat3(Modelview); - Ray = Rotation * sky_ray(vPosition*0.5 + 0.5); // map [-1,1] -> [0,1] - // Should disable depth test when rendering. - gl_Position = vec4(vPosition.xy, 0.99999, 1.0); + vec2 FilmPosition = vPosition * 0.5; // map [-1,1] -> [-1/2, +1/2] + FilmPosition *= Flip; + Ray = mat3(CameraRotation) * sky_ray(FilmPosition); + // TODO: Should disable depth test when rendering. + gl_Position = vec4(vPosition, 0.99999, 1.0); // z=1 -> send to background + + // DEBUG + // Texcoord = FilmPosition + 0.5; + // Texcoord.y = 1.0 - Texcoord.y; } diff --git a/gfx/shaders/irradiance_map.frag b/gfx/shaders/irradiance_map.frag index 3cfe5b2..8200e73 100644 --- a/gfx/shaders/irradiance_map.frag +++ b/gfx/shaders/irradiance_map.frag @@ -13,6 +13,9 @@ uniform samplerCube Sky; in vec3 Ray; +// DEBUG +// in vec2 Texcoord; + layout (location = 0) out vec4 Color; void main() @@ -29,8 +32,8 @@ void main() // T vec3 N = normalize(Ray); vec3 B = (abs(N.x) - 1.0 <= EPS) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); - vec3 T = cross(B, N); - B = cross(N, T); + vec3 T = normalize(cross(B, N)); + B = normalize(cross(N, T)); int num_samples = 0; vec3 irradiance = vec3(0.0); @@ -54,6 +57,8 @@ void main() irradiance = PI * irradiance / float(num_samples); // For debugging in trace. + //irradiance = texture(Sky, Ray).rgb; + // irradiance = vec3(Texcoord, 0.0); //irradiance = pow(irradiance, vec3(1.0/2.2)); Color = vec4(irradiance, 1.0); diff --git a/gfx/shaders/skyquad.frag b/gfx/shaders/skyquad.frag index 39af013..9b44bfd 100644 --- a/gfx/shaders/skyquad.frag +++ b/gfx/shaders/skyquad.frag @@ -8,4 +8,6 @@ void main() { vec3 R = normalize(Ray); Colour = vec4(pow(texture(Skyquad, R).rgb, vec3(1.0/2.2)), 1.0); + // Debug. + //Colour = vec4(pow(R*0.5 + 0.5, vec3(1.0 / 2.2)), 1.0); } diff --git a/gfx/shaders/skyquad.vert b/gfx/shaders/skyquad.vert index f0658d6..c0c46e6 100644 --- a/gfx/shaders/skyquad.vert +++ b/gfx/shaders/skyquad.vert @@ -1,4 +1,4 @@ -uniform mat4 Modelview; +uniform mat4 CameraRotation; // From camera space to world space. uniform float Fovy; uniform float Aspect; @@ -8,21 +8,20 @@ out vec3 Ray; // We would typically normalize the vector, but there is no need to do so when // sampling a cube map. -vec3 sky_ray(vec2 Texcoord) +vec3 sky_ray(vec2 FilmPosition) { //float d = 0.5 / tan(Fovy/2.0); - // return vec3((Texcoord.x - 0.5) * Aspect, - // Texcoord.y - 0.5, + // return vec3((FilmPosition.x - 0.5) * Aspect, + // FilmPosition.y - 0.5, // -d); float d = 0.5 / tan(Fovy/2.0); - return vec3((Texcoord.x - 0.5), - Texcoord.y - 0.5, - -d); + return vec3(FilmPosition, -d); } void main() { - mat3 Rotation = mat3(Modelview); - Ray = Rotation * sky_ray(Position*0.5 + 0.5); // map [-1,1] -> [0,1] + vec2 FilmPosition = Position * 0.5; // map [-1,1] -> [-1/2, +1/2] + Ray = mat3(CameraRotation) * sky_ray(FilmPosition); + // Set z to the background. gl_Position = vec4(Position, 0.99999, 1.0); // z=1 -> send to background } diff --git a/gfx/src/renderer/renderer.c b/gfx/src/renderer/renderer.c index 47f1344..b0bef33 100644 --- a/gfx/src/renderer/renderer.c +++ b/gfx/src/renderer/renderer.c @@ -114,6 +114,7 @@ typedef struct RenderState { Renderer* renderer; const Scene* scene; const Camera* camera; + const mat4* camera_rotation; // From camera to world space, rotation only. const mat4* view_matrix; const mat4* projection; Light* environment_light; @@ -167,6 +168,8 @@ static void draw_recursively( const SceneObject* object = mem_get_object(node->object); assert(object); + // TODO: Avoid computing matrices like Modelview or MVP if the shader does + // not use them. const mat4 model_matrix = mat4_mul(node_transform, object->transform); const mat4 modelview = mat4_mul(*state->view_matrix, model_matrix); const mat4 mvp = mat4_mul(*state->projection, modelview); @@ -187,13 +190,13 @@ static void draw_recursively( assert(mesh->geometry); assert(mesh->material); // Apply common shader uniforms not captured by materials. - // TODO: Avoid computing matrices like Modelview or MVP if the shader does - // not use them. ShaderProgram* shader = mesh->shader; gfx_set_mat4_uniform(shader, "ModelMatrix", &model_matrix); gfx_set_mat4_uniform(shader, "Modelview", &modelview); + gfx_set_mat4_uniform(shader, "View", state->view_matrix); gfx_set_mat4_uniform(shader, "Projection", state->projection); gfx_set_mat4_uniform(shader, "MVP", &mvp); + gfx_set_mat4_uniform(shader, "CameraRotation", state->camera_rotation); gfx_set_float_uniform(shader, "Fovy", state->fovy); gfx_set_float_uniform(shader, "Aspect", state->aspect); gfx_set_vec3_uniform(shader, "CameraPosition", state->camera->spatial.p); @@ -247,6 +250,8 @@ void gfx_render_scene( } const mat4 projection = camera ? camera->camera.projection : mat4_id(); + const mat4 camera_rotation = + mat4_rotation(spatial3_transform(&camera->camera.spatial)); const mat4 view_matrix = camera ? spatial3_inverse_transform(&camera->camera.spatial) : mat4_id(); @@ -259,6 +264,7 @@ void gfx_render_scene( .renderer = renderer, .scene = scene, .camera = &camera->camera, + .camera_rotation = &camera_rotation, .view_matrix = &view_matrix, .projection = &projection, .environment_light = 0, diff --git a/gfx/src/util/ibl.c b/gfx/src/util/ibl.c index c9bef43..6b7465c 100644 --- a/gfx/src/util/ibl.c +++ b/gfx/src/util/ibl.c @@ -24,7 +24,16 @@ static const CubemapFace faces[6] = { CubemapFacePosY, // Up. CubemapFaceNegY, // Down. CubemapFacePosZ, // Back. - CubemapFaceNegZ, // Forward. + CubemapFaceNegZ, // Front. +}; + +static const float flips[6] = { + -1.0f, // Right. + -1.0f, // Left. + +1.0f, // Up. + +1.0f, // Down. + -1.0f, // Back. + -1.0f, // Front. }; IBL* gfx_make_ibl(RenderBackend* render_backend) { @@ -70,6 +79,9 @@ IBL* gfx_make_ibl(RenderBackend* render_backend) { goto cleanup; } + // TODO: Debug the camera rotations. Irradiance debug output should appear + // just like the input cubemap. + // Right. ibl->rotations[0] = mat4_lookat( /*position=*/vec3_make(0, 0, 0), @@ -95,7 +107,7 @@ IBL* gfx_make_ibl(RenderBackend* render_backend) { /*position=*/vec3_make(0, 0, 0), /*target=*/vec3_make(0, 0, 1), /*up=*/vec3_make(0, 1, 0)); - // Forward. + // Front. ibl->rotations[5] = mat4_lookat( /*position=*/vec3_make(0, 0, 0), /*target=*/vec3_make(0, 0, -1), @@ -226,8 +238,9 @@ Texture* gfx_make_irradiance_map( .cubemap.texture = irradiance_map})) { goto cleanup; } + gfx_set_float_uniform(ibl->irradiance_map_shader, "Flip", flips[i]); gfx_set_mat4_uniform( - ibl->irradiance_map_shader, "Modelview", &ibl->rotations[i]); + ibl->irradiance_map_shader, "CameraRotation", &ibl->rotations[i]); gfx_apply_uniforms(ibl->irradiance_map_shader); gfx_render_geometry(ibl->quad); } @@ -291,8 +304,10 @@ Texture* gfx_make_prefiltered_environment_map( .cubemap.texture = prefiltered_env_map})) { goto cleanup; } + gfx_set_float_uniform( + ibl->prefiltered_environment_map_shader, "Flip", flips[i]); gfx_set_mat4_uniform( - ibl->prefiltered_environment_map_shader, "Modelview", + ibl->prefiltered_environment_map_shader, "CameraRotation", &ibl->rotations[i]); gfx_apply_uniforms(ibl->prefiltered_environment_map_shader); gfx_render_geometry(ibl->quad); diff --git a/gfx/src/util/texture.c b/gfx/src/util/texture.c index 99241f4..0727dae 100644 --- a/gfx/src/util/texture.c +++ b/gfx/src/util/texture.c @@ -6,6 +6,42 @@ #define STB_IMAGE_IMPLEMENTATION #include +#include + +static void flip_horizontally( + unsigned char* pixels, int width, int height, int components) { + assert(pixels); + + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width / 2; ++x) { + unsigned char* p1 = &pixels[(y * width + x) * components]; + unsigned char* p2 = &pixels[(y * width + (width - x - 1)) * components]; + + for (int c = 0; c < components; ++c) { + unsigned char tmp = *p1; + *p1 = *p2; + *p2 = tmp; + p1++; + p2++; + } + } + } +} + +// Note that the cubemap coordinate system uses the one in RenderMan: +// +// https://www.khronos.org/opengl/wiki/Cubemap_Texture +// +// This is what happens: +// +// - Cubemaps follow a left-handed coordinate system. Say, +X is right, +Y is +// up, and +Z is forward. +// - The texture coordinate system follow's DirectX's, so +V goes down, not up +// like it does in OpenGL. +// +// For this reason, we do X and Y flips when doing cubemap textures so that we +// can sample cubemaps as if they were given in the usual OpenGL coordinate +// system. Texture* gfx_load_texture( RenderBackend* render_backend, const LoadTextureCmd* cmd) { assert(render_backend); @@ -21,6 +57,7 @@ Texture* gfx_load_texture( switch (cmd->type) { case LoadTexture: { const char* filepath = mstring_cstr(&cmd->data.texture.filepath); + stbi_set_flip_vertically_on_load(0); pixels[0] = stbi_load(filepath, &width, &height, &components, 0); if (!pixels[0]) { gfx_set_error("Failed to load texture file: %s", filepath); @@ -29,6 +66,8 @@ Texture* gfx_load_texture( } case LoadCubemap: for (int i = 0; i < 6; ++i) { + // Flip +Y and -Y textures vertically. + stbi_set_flip_vertically_on_load(((i == 2) || (i == 3)) ? 1 : 0); const char* filepath = mstring_cstr(&cmd->data.cubemap.filepaths.filepath_pos_x + i); stbi_uc* image_pixels = @@ -43,6 +82,9 @@ Texture* gfx_load_texture( "components"); break; } + if ((i != 2) && (i != 3)) { + flip_horizontally(image_pixels, width, height, components); + } pixels[i] = image_pixels; old_components = components; } diff --git a/gltfview/src/game.c b/gltfview/src/game.c index 1db7cba..f822b08 100644 --- a/gltfview/src/game.c +++ b/gltfview/src/game.c @@ -70,8 +70,8 @@ static Texture* load_environment_map(RenderBackend* render_backend) { mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"), mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"), mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_north.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_south.bmp")} + mstring_make("/assets/skybox/clouds1/clouds1_south.bmp"), + mstring_make("/assets/skybox/clouds1/clouds1_north.bmp")} }); } @@ -222,7 +222,7 @@ bool game_new(Game* game, int argc, const char** argv) { // false}); const bool play_result = gfx_play_animation( anima, &(AnimationPlaySettings){.name = "Walk", .loop = true}); - assert(play_result); + // assert(play_result); return true; @@ -251,6 +251,15 @@ void game_update(Game* game, double t, double dt) { /*radius=*/2.5, /*azimuth=*/t * 0.5, /*zenith=*/0); spatial3_lookat(&camera->spatial, orbit_point); + + // spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2)); + // spatial3_lookat(&camera->spatial, vec3_make(0, 0, -1)); + + // spatial3_orbit( + // &camera->spatial, vec3_make(0, 0, 0), + // /*radius=*/2.5, + // /*azimuth=*/t * 0.2, /*zenith=*/0); + // spatial3_lookat(&camera->spatial, vec3_make(0, 0, 0)); } void game_render(const Game* game) { -- cgit v1.2.3