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 <stb_image.h>
 
+#include <assert.h>
+
+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