From 9441608623a2692263191c254db23765eefa2cef Mon Sep 17 00:00:00 2001
From: Cosmic Linden <cosmic@lindenlab.com>
Date: Mon, 6 May 2024 10:07:02 -0700
Subject: secondlife/viewer#907: Local PBR terrain texture transforms

---
 indra/newview/app_settings/settings.xml            | 220 +++++++++++++++++++++
 .../shaders/class1/deferred/pbrterrainF.glsl       |  93 +++++++--
 .../shaders/class1/deferred/pbrterrainUtilF.glsl   |  19 +-
 .../shaders/class1/deferred/pbrterrainV.glsl       | 135 ++++++++++---
 .../shaders/class1/deferred/textureUtilV.glsl      |  39 ++++
 indra/newview/lldrawpoolterrain.cpp                |  64 ++++--
 indra/newview/llviewercontrol.cpp                  |  45 ++++-
 indra/newview/llvlcomposition.cpp                  |  77 ++++++--
 indra/newview/llvlcomposition.h                    |  11 +-
 9 files changed, 606 insertions(+), 97 deletions(-)

(limited to 'indra/newview')

diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index af63d6ff87..c3f9a64fd6 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -16828,6 +16828,226 @@
       <string>String</string>
       <key>Value</key>
       <string>00000000-0000-0000-0000-000000000000</string>
+    </map>
+    <key>LocalTerrainTransform1ScaleU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset1 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform1ScaleV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset1 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform1Rotation</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset1 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform1OffsetU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset1 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform1OffsetV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset1 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform2ScaleU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset2 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform2ScaleV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset2 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform2Rotation</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset2 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform2OffsetU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset2 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform2OffsetV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset2 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform3ScaleU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset3 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform3ScaleV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset3 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform3Rotation</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset3 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform3OffsetU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset3 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform3OffsetV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset3 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform4ScaleU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset4 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform4ScaleV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset4 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>1.0</real>
+    </map>
+    <key>LocalTerrainTransform4Rotation</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset4 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform4OffsetU</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset4 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
+    </map>
+    <key>LocalTerrainTransform4OffsetV</key>
+    <map>
+      <key>Comment</key>
+      <string>KHR texture transform component if LocalTerrainAsset4 is set</string>
+      <key>Persist</key>
+      <integer>0</integer>
+      <key>Type</key>
+      <string>F32</string>
+      <key>Value</key>
+      <real>0.0</real>
     </map>
 	<key>PathfindingRetrieveNeighboringRegion</key>
     <map>
diff --git a/indra/newview/app_settings/shaders/class1/deferred/pbrterrainF.glsl b/indra/newview/app_settings/shaders/class1/deferred/pbrterrainF.glsl
index de4745c1c4..65a6b8613d 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/pbrterrainF.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/pbrterrainF.glsl
@@ -31,7 +31,7 @@
 #define TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS -3
 
 #if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
-#define TerrainCoord vec4[2]
+#define TerrainCoord vec4[3]
 #elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
 #define TerrainCoord vec2
 #endif
@@ -131,12 +131,16 @@ uniform vec3[4] emissiveColors;
 uniform vec4 minimum_alphas; // PBR alphaMode: MASK, See: mAlphaCutoff, setAlphaCutoff()
 
 #if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
+in vec4[10] vary_coords;
+#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
 in vec4[2] vary_coords;
 #endif
 in vec3 vary_position;
 in vec3 vary_normal;
-in vec3 vary_tangent;
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+in vec3 vary_tangents[4];
 flat in float vary_sign;
+#endif
 in vec4 vary_texcoord0;
 in vec4 vary_texcoord1;
 
@@ -144,17 +148,26 @@ void mirrorClip(vec3 position);
 
 float terrain_mix(TerrainMix tm, vec4 tms4);
 
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+// from mikktspace.com
+vec3 mikktspace(vec3 vNt, vec3 vT)
+{
+    vec3 vN = vary_normal;
+    
+    vec3 vB = vary_sign * cross(vN, vT);
+    vec3 tnorm = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN );
+
+    tnorm *= gl_FrontFacing ? 1.0 : -1.0;
+
+    return tnorm;
+}
+#endif
+
 void main()
 {
     // Make sure we clip the terrain if we're in a mirror.
     mirrorClip(vary_position);
 
-#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
-    TerrainCoord terrain_texcoord = vary_coords;
-#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
-    TerrainCoord terrain_texcoord = vary_texcoord0.xy;
-#endif
-
     float alpha1 = texture(alpha_ramp, vary_texcoord0.zw).a;
     float alpha2 = texture(alpha_ramp,vary_texcoord1.xy).a;
     float alphaFinal = texture(alpha_ramp, vary_texcoord1.zw).a;
@@ -185,6 +198,16 @@ void main()
     switch (tm.type & MIX_X)
     {
     case MIX_X:
+#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
+        TerrainCoord terrain_texcoord;
+        terrain_texcoord[0].xy = vary_coords[0].xy;
+        terrain_texcoord[0].zw = vary_coords[0].zw;
+        terrain_texcoord[1].xy = vary_coords[1].xy;
+        terrain_texcoord[1].zw = vary_coords[1].zw;
+        terrain_texcoord[2].xy = vary_coords[2].xy;
+#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
+        TerrainCoord terrain_texcoord = vary_coords[0].xy;
+#endif
         mix2 = terrain_sample_and_multiply_pbr(
             terrain_texcoord
             , detail_0_base_color
@@ -207,6 +230,9 @@ void main()
             , emissiveColors[0]
 #endif
         );
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+        mix2.vNt = mikktspace(mix2.vNt, vary_tangents[0]);
+#endif
         mix = mix_pbr(mix, mix2, tm.weight.x);
         break;
     default:
@@ -215,6 +241,16 @@ void main()
     switch (tm.type & MIX_Y)
     {
     case MIX_Y:
+#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
+        TerrainCoord terrain_texcoord;
+        terrain_texcoord[0].xy = vary_coords[2].zw;
+        terrain_texcoord[0].zw = vary_coords[3].xy;
+        terrain_texcoord[1].xy = vary_coords[3].zw;
+        terrain_texcoord[1].zw = vary_coords[4].xy;
+        terrain_texcoord[2].xy = vary_coords[4].zw;
+#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
+        TerrainCoord terrain_texcoord = vary_coords[0].zw;
+#endif
         mix2 = terrain_sample_and_multiply_pbr(
             terrain_texcoord
             , detail_1_base_color
@@ -237,6 +273,9 @@ void main()
             , emissiveColors[1]
 #endif
         );
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+        mix2.vNt = mikktspace(mix2.vNt, vary_tangents[1]);
+#endif
         mix = mix_pbr(mix, mix2, tm.weight.y);
         break;
     default:
@@ -245,6 +284,16 @@ void main()
     switch (tm.type & MIX_Z)
     {
     case MIX_Z:
+#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
+        TerrainCoord terrain_texcoord;
+        terrain_texcoord[0].xy = vary_coords[5].xy;
+        terrain_texcoord[0].zw = vary_coords[5].zw;
+        terrain_texcoord[1].xy = vary_coords[6].xy;
+        terrain_texcoord[1].zw = vary_coords[6].zw;
+        terrain_texcoord[2].xy = vary_coords[7].xy;
+#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
+        TerrainCoord terrain_texcoord = vary_coords[1].xy;
+#endif
         mix2 = terrain_sample_and_multiply_pbr(
             terrain_texcoord
             , detail_2_base_color
@@ -267,6 +316,9 @@ void main()
             , emissiveColors[2]
 #endif
         );
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+        mix2.vNt = mikktspace(mix2.vNt, vary_tangents[2]);
+#endif
         mix = mix_pbr(mix, mix2, tm.weight.z);
         break;
     default:
@@ -275,6 +327,16 @@ void main()
     switch (tm.type & MIX_W)
     {
     case MIX_W:
+#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
+        TerrainCoord terrain_texcoord;
+        terrain_texcoord[0].xy = vary_coords[7].zw;
+        terrain_texcoord[0].zw = vary_coords[8].xy;
+        terrain_texcoord[1].xy = vary_coords[8].zw;
+        terrain_texcoord[1].zw = vary_coords[9].xy;
+        terrain_texcoord[2].xy = vary_coords[9].zw;
+#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
+        TerrainCoord terrain_texcoord = vary_coords[1].zw;
+#endif
         mix2 = terrain_sample_and_multiply_pbr(
             terrain_texcoord
             , detail_3_base_color
@@ -297,6 +359,9 @@ void main()
             , emissiveColors[3]
 #endif
         );
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+        mix2.vNt = mikktspace(mix2.vNt, vary_tangents[3]);
+#endif
         mix = mix_pbr(mix, mix2, tm.weight.w);
         break;
     default:
@@ -311,19 +376,11 @@ void main()
     float base_color_factor_alpha = terrain_mix(tm, vec4(baseColorFactors[0].z, baseColorFactors[1].z, baseColorFactors[2].z, baseColorFactors[3].z));
 
 #if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
-    // from mikktspace.com
-    vec3 vNt = mix.vNt;
-    vec3 vN = vary_normal;
-    vec3 vT = vary_tangent.xyz;
-    
-    vec3 vB = vary_sign * cross(vN, vT);
-    vec3 tnorm = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN );
-
-    tnorm *= gl_FrontFacing ? 1.0 : -1.0;
+    vec3 tnorm = normalize(mix.vNt);
 #else
     vec3 tnorm = vary_normal;
-    tnorm *= gl_FrontFacing ? 1.0 : -1.0;
 #endif
+    tnorm *= gl_FrontFacing ? 1.0 : -1.0;
    
 
 #if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
diff --git a/indra/newview/app_settings/shaders/class1/deferred/pbrterrainUtilF.glsl b/indra/newview/app_settings/shaders/class1/deferred/pbrterrainUtilF.glsl
index 935c3f9301..7a7fd783ec 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/pbrterrainUtilF.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/pbrterrainUtilF.glsl
@@ -233,17 +233,12 @@ float terrain_mix(TerrainMix tm, vec4 tms4)
 // Triplanar mapping
 
 // Pre-transformed texture coordinates for each axial uv slice (Packing: xy, yz, (-x)z, unused)
-#define TerrainCoord vec4[2]
+#define TerrainCoord vec4[3]
 
-vec2 _t_uv(vec2 uv_unflipped, float sign_or_zero)
+// If sign_or_zero is positive, use uv_unflippped, otherwise use uv_flipped
+vec2 _t_uv(vec2 uv_unflipped, vec2 uv_flipped, float sign_or_zero)
 {
-    // Handle case where sign is 0
-    float sign = (2.0*sign_or_zero) + 1.0;
-    sign /= abs(sign);
-    // If the vertex normal is negative, flip the texture back
-    // right-side up.
-    vec2 uv = uv_unflipped * vec2(sign, 1);
-    return uv;
+    return mix(uv_flipped, uv_unflipped, max(0.0, sign_or_zero));
 }
 
 vec3 _t_normal_post_1(vec3 vNt0, float sign_or_zero)
@@ -298,9 +293,9 @@ PBRMix terrain_sample_pbr(
 {
     PBRMix mix = init_pbr_mix();
 
-#define get_uv_x() _t_uv(terrain_coord[0].zw, sign(vary_vertex_normal.x))
-#define get_uv_y() _t_uv(terrain_coord[1].xy, sign(vary_vertex_normal.y))
-#define get_uv_z() _t_uv(terrain_coord[0].xy, sign(vary_vertex_normal.z))
+#define get_uv_x() _t_uv(terrain_coord[0].zw, terrain_coord[1].zw, sign(vary_vertex_normal.x))
+#define get_uv_y() _t_uv(terrain_coord[1].xy, terrain_coord[2].xy, sign(vary_vertex_normal.y))
+#define get_uv_z() _t_uv(terrain_coord[0].xy, vec2(0),             sign(vary_vertex_normal.z))
     switch (tw.type & SAMPLE_X)
     {
     case SAMPLE_X:
diff --git a/indra/newview/app_settings/shaders/class1/deferred/pbrterrainV.glsl b/indra/newview/app_settings/shaders/class1/deferred/pbrterrainV.glsl
index 489fc26e3f..167d980eb8 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/pbrterrainV.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/pbrterrainV.glsl
@@ -23,6 +23,11 @@
  * $/LicenseInfo$
  */
 
+#define TERRAIN_PBR_DETAIL_EMISSIVE 0
+#define TERRAIN_PBR_DETAIL_OCCLUSION -1
+#define TERRAIN_PBR_DETAIL_NORMAL -2
+#define TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS -3
+
 uniform mat3 normal_matrix;
 uniform mat4 texture_matrix0;
 uniform mat4 modelview_matrix;
@@ -34,24 +39,28 @@ in vec4 tangent;
 in vec4 diffuse_color;
 in vec2 texcoord1;
 
-#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
-out vec4[2] vary_coords;
-#endif
 out vec3 vary_vertex_normal; // Used by pbrterrainUtilF.glsl
 out vec3 vary_normal;
-out vec3 vary_tangent;
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+out vec3 vary_tangents[4];
 flat out float vary_sign;
+#endif
 out vec4 vary_texcoord0;
 out vec4 vary_texcoord1;
+#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
+out vec4[10] vary_coords;
+#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
+out vec4[2] vary_coords;
+#endif
 out vec3 vary_position;
 
-// *HACK: tangent_space_transform should use texture_normal_transform, or maybe
-// we shouldn't use tangent_space_transform at all. See the call to
-// tangent_space_transform below.
-uniform vec4[2] texture_base_color_transform;
+// *HACK: Each material uses only one texture transform, but the KHR texture
+// transform spec allows handling texture transforms separately for each
+// individual texture info.
+uniform vec4[5] terrain_texture_transforms;
 
-vec2 texture_transform(vec2 vertex_texcoord, vec4[2] khr_gltf_transform, mat4 sl_animation_transform);
-vec3 tangent_space_transform(vec4 vertex_tangent, vec3 vertex_normal, vec4[2] khr_gltf_transform, mat4 sl_animation_transform);
+vec2 terrain_texture_transform(vec2 vertex_texcoord, vec4[2] khr_gltf_transform);
+vec3 terrain_tangent_space_transform(vec4 vertex_tangent, vec3 vertex_normal, vec4[2] khr_gltf_transform);
 
 void main()
 {
@@ -63,31 +72,101 @@ void main()
     vary_vertex_normal = normal;
 	vec3 t = normal_matrix * tangent.xyz;
 
-    vary_tangent = normalize(t);
-    // *TODO: Decide if we want this. It may be better to just calculate the
-    // tangents on-the-fly in the fragment shader, due to the subtleties of the
-    // effect of triplanar mapping on UVs.
-    // *HACK: Should be using texture_normal_transform here. The KHR texture
-    // transform spec requires handling texture transforms separately for each
-    // individual texture.
-    vary_tangent = normalize(tangent_space_transform(vec4(t, tangent.w), n, texture_base_color_transform, texture_matrix0));
+#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
+    {
+        vec4[2] ttt;
+        // material 1
+        ttt[0].xyz = terrain_texture_transforms[0].xyz;
+        ttt[1].x = terrain_texture_transforms[0].w;
+        ttt[1].y = terrain_texture_transforms[1].x;
+        vary_tangents[0] = normalize(terrain_tangent_space_transform(vec4(t, tangent.w), n, ttt));
+        // material 2
+        ttt[0].xyz = terrain_texture_transforms[1].yzw;
+        ttt[1].xy = terrain_texture_transforms[2].xy;
+        vary_tangents[1] = normalize(terrain_tangent_space_transform(vec4(t, tangent.w), n, ttt));
+        // material 3
+        ttt[0].xy = terrain_texture_transforms[2].zw;
+        ttt[0].z = terrain_texture_transforms[3].x;
+        ttt[1].xy = terrain_texture_transforms[3].yz;
+        vary_tangents[2] = normalize(terrain_tangent_space_transform(vec4(t, tangent.w), n, ttt));
+        // material 4
+        ttt[0].x = terrain_texture_transforms[3].w;
+        ttt[0].yz = terrain_texture_transforms[4].xy;
+        ttt[1].xy = terrain_texture_transforms[4].zw;
+        vary_tangents[3] = normalize(terrain_tangent_space_transform(vec4(t, tangent.w), n, ttt));
+    }
+
     vary_sign = tangent.w;
+#endif
     vary_normal = normalize(n);
 
     // Transform and pass tex coords
-    // *HACK: texture_base_color_transform is used for all of these here, but
-    // the KHR texture transform spec requires handling texture transforms
-    // separately for each individual texture.
+    {
+        vec4[2] ttt;
 #if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
-    // xy
-    vary_coords[0].xy = texture_transform(position.xy, texture_base_color_transform, texture_matrix0);
-    // yz
-    vary_coords[0].zw = texture_transform(position.yz, texture_base_color_transform, texture_matrix0);
-    // (-x)z
-    vary_coords[1].xy = texture_transform(position.xz * vec2(-1, 1), texture_base_color_transform, texture_matrix0);
+// Don't care about upside-down (transform_xy_flipped())
+#define transform_xy()             terrain_texture_transform(position.xy,               ttt)
+#define transform_yz()             terrain_texture_transform(position.yz,               ttt)
+#define transform_negx_z()         terrain_texture_transform(position.xz * vec2(-1, 1), ttt)
+#define transform_yz_flipped()     terrain_texture_transform(position.yz * vec2(-1, 1), ttt)
+#define transform_negx_z_flipped() terrain_texture_transform(position.xz,               ttt)
+        // material 1
+        ttt[0].xyz = terrain_texture_transforms[0].xyz;
+        ttt[1].x = terrain_texture_transforms[0].w;
+        ttt[1].y = terrain_texture_transforms[1].x;
+        vary_coords[0].xy = transform_xy();
+        vary_coords[0].zw = transform_yz();
+        vary_coords[1].xy = transform_negx_z();
+        vary_coords[1].zw = transform_yz_flipped();
+        vary_coords[2].xy = transform_negx_z_flipped();
+        // material 2
+        ttt[0].xyz = terrain_texture_transforms[1].yzw;
+        ttt[1].xy = terrain_texture_transforms[2].xy;
+        vary_coords[2].zw = transform_xy();
+        vary_coords[3].xy = transform_yz();
+        vary_coords[3].zw = transform_negx_z();
+        vary_coords[4].xy = transform_yz_flipped();
+        vary_coords[4].zw = transform_negx_z_flipped();
+        // material 3
+        ttt[0].xy = terrain_texture_transforms[2].zw;
+        ttt[0].z = terrain_texture_transforms[3].x;
+        ttt[1].xy = terrain_texture_transforms[3].yz;
+        vary_coords[5].xy = transform_xy();
+        vary_coords[5].zw = transform_yz();
+        vary_coords[6].xy = transform_negx_z();
+        vary_coords[6].zw = transform_yz_flipped();
+        vary_coords[7].xy = transform_negx_z_flipped();
+        // material 4
+        ttt[0].x = terrain_texture_transforms[3].w;
+        ttt[0].yz = terrain_texture_transforms[4].xy;
+        ttt[1].xy = terrain_texture_transforms[4].zw;
+        vary_coords[7].zw = transform_xy();
+        vary_coords[8].xy = transform_yz();
+        vary_coords[8].zw = transform_negx_z();
+        vary_coords[9].xy = transform_yz_flipped();
+        vary_coords[9].zw = transform_negx_z_flipped();
 #elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
-    vary_texcoord0.xy = texture_transform(position.xy, texture_base_color_transform, texture_matrix0);
+        // material 1
+        ttt[0].xyz = terrain_texture_transforms[0].xyz;
+        ttt[1].x = terrain_texture_transforms[0].w;
+        ttt[1].y = terrain_texture_transforms[1].x;
+        vary_coords[0].xy = terrain_texture_transform(position.xy, ttt);
+        // material 2
+        ttt[0].xyz = terrain_texture_transforms[1].yzw;
+        ttt[1].xy = terrain_texture_transforms[2].xy;
+        vary_coords[0].zw = terrain_texture_transform(position.xy, ttt);
+        // material 3
+        ttt[0].xy = terrain_texture_transforms[2].zw;
+        ttt[0].z = terrain_texture_transforms[3].x;
+        ttt[1].xy = terrain_texture_transforms[3].yz;
+        vary_coords[1].xy = terrain_texture_transform(position.xy, ttt);
+        // material 4
+        ttt[0].x = terrain_texture_transforms[3].w;
+        ttt[0].yz = terrain_texture_transforms[4].xy;
+        ttt[1].xy = terrain_texture_transforms[4].zw;
+        vary_coords[1].zw = terrain_texture_transform(position.xy, ttt);
 #endif
+    }
     
     vec4 tc = vec4(texcoord1,0,1);
     vary_texcoord0.zw = tc.xy;
diff --git a/indra/newview/app_settings/shaders/class1/deferred/textureUtilV.glsl b/indra/newview/app_settings/shaders/class1/deferred/textureUtilV.glsl
index 732333311c..baf4010fa7 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/textureUtilV.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/textureUtilV.glsl
@@ -48,6 +48,7 @@ vec2 khr_texture_transform(vec2 texcoord, vec2 scale, float rotation, vec2 offse
     return (transform * vec3(texcoord, 1)).xy;
 }
 
+// A texture transform function for PBR materials applied to shape prims/Collada model prims
 // vertex_texcoord - The UV texture coordinates sampled from the vertex at
 //     runtime. Per SL convention, this is in a right-handed UV coordinate
 //     system. Collada models also have right-handed UVs.
@@ -77,6 +78,21 @@ vec2 texture_transform(vec2 vertex_texcoord, vec4[2] khr_gltf_transform, mat4 sl
     return texcoord;
 }
 
+// Similar to texture_transform but no offset during coordinate system
+// conversion, and no texture animation support.
+vec2 terrain_texture_transform(vec2 vertex_texcoord, vec4[2] khr_gltf_transform)
+{
+    vec2 texcoord = vertex_texcoord;
+
+    texcoord.y = 1.0 - texcoord.y;
+    //texcoord.y = -texcoord.y;
+    texcoord = khr_texture_transform(texcoord, khr_gltf_transform[0].xy, khr_gltf_transform[0].z, khr_gltf_transform[1].xy);
+    texcoord.y = 1.0 - texcoord.y;
+    //texcoord.y = -texcoord.y;
+
+    return texcoord;
+}
+
 // Take the rotation only from both transforms and apply to the tangent. This
 // accounts for the change of the topology of the normal texture when a texture
 // rotation is applied to it.
@@ -120,3 +136,26 @@ vec3 tangent_space_transform(vec4 vertex_tangent, vec3 vertex_normal, vec4[2] kh
 
     return (weights.x * vertex_binormal.xyz) + (weights.y * vertex_tangent.xyz);
 }
+
+// Similar to tangent_space_transform but no offset during coordinate system
+// conversion, and no texture animation support.
+vec3 terrain_tangent_space_transform(vec4 vertex_tangent, vec3 vertex_normal, vec4[2] khr_gltf_transform)
+{
+    // Immediately convert to left-handed coordinate system ((0,1) -> (0, -1))
+    vec2 weights = vec2(0, -1);
+
+    // Apply KHR_texture_transform (rotation only)
+    float khr_rotation = khr_gltf_transform[0].z;
+    mat2 khr_rotation_mat = mat2(
+        cos(khr_rotation),-sin(khr_rotation),
+        sin(khr_rotation), cos(khr_rotation)
+    );
+    weights = khr_rotation_mat * weights;
+
+    // Convert back to right-handed coordinate system
+    weights.y = -weights.y;
+
+    vec3 vertex_binormal = vertex_tangent.w * cross(vertex_normal, vertex_tangent.xyz);
+
+    return (weights.x * vertex_binormal.xyz) + (weights.y * vertex_tangent.xyz);
+}
diff --git a/indra/newview/lldrawpoolterrain.cpp b/indra/newview/lldrawpoolterrain.cpp
index c5932a6ad9..de48d315e6 100644
--- a/indra/newview/lldrawpoolterrain.cpp
+++ b/indra/newview/lldrawpoolterrain.cpp
@@ -330,7 +330,7 @@ void LLDrawPoolTerrain::renderFullShaderPBR(BOOL local_materials)
 	// Hack! Get the region that this draw pool is rendering from!
 	LLViewerRegion *regionp = mDrawFace[0]->getDrawable()->getVObj()->getRegion();
 	LLVLComposition *compp = regionp->getComposition();
-	LLPointer<LLFetchedGLTFMaterial> (*fetched_materials)[LLVLComposition::ASSET_COUNT] = &compp->mDetailMaterials;
+	LLPointer<LLFetchedGLTFMaterial> (*fetched_materials)[LLVLComposition::ASSET_COUNT] = &compp->mDetailRenderMaterials;
 
 	constexpr U32 terrain_material_count = LLVLComposition::ASSET_COUNT;
 #ifdef SHOW_ASSERT
@@ -341,7 +341,7 @@ void LLDrawPoolTerrain::renderFullShaderPBR(BOOL local_materials)
     if (local_materials)
     {
         // Override region terrain with the global local override terrain
-		fetched_materials = &gLocalTerrainMaterials.mDetailMaterials;
+		fetched_materials = &gLocalTerrainMaterials.mDetailRenderMaterials;
     }
 	const LLGLTFMaterial* materials[terrain_material_count];
 	for (U32 i = 0; i < terrain_material_count; ++i)
@@ -433,21 +433,51 @@ void LLDrawPoolTerrain::renderFullShaderPBR(BOOL local_materials)
 	llassert(shader);
 		
 
-    // *TODO: Figure out why this offset is *sometimes* producing seams at the
-    // region edge, and repeat jumps when crossing regions, when
-    // RenderTerrainPBRScale is not a factor of the region scale.
-	LLVector3d region_origin_global = gAgent.getRegion()->getOriginGlobal();
-	F32 offset_x = (F32)fmod(region_origin_global.mdV[VX], 1.0/(F64)sPBRDetailScale)*sPBRDetailScale;
-	F32 offset_y = (F32)fmod(region_origin_global.mdV[VY], 1.0/(F64)sPBRDetailScale)*sPBRDetailScale;
-
-    LLGLTFMaterial::TextureTransform base_color_transform;
-    base_color_transform.mScale = LLVector2(sPBRDetailScale, sPBRDetailScale);
-    base_color_transform.mOffset = LLVector2(offset_x, offset_y);
-    F32 base_color_packed[8];
-    base_color_transform.getPacked(base_color_packed);
-    // *HACK: Use the same texture repeats for all PBR terrain textures for now
-    // (not compliant with KHR texture transform spec)
-    shader->uniform4fv(LLShaderMgr::TEXTURE_BASE_COLOR_TRANSFORM, 2, (F32*)base_color_packed);
+    // Like for PBR materials, PBR terrain texture transforms are defined by
+    // the KHR_texture_transform spec, but with the following notable
+    // differences:
+    //   1) The PBR UV origin is defined as the Southwest corner of the region,
+    //      with positive U facing East and positive V facing South.
+    //   2) There is an additional scaling factor RenderTerrainPBRScale. If
+    //      we've done our math right, RenderTerrainPBRScale should not affect the
+    //      overall behavior of KHR_texture_transform
+    //   3) There is only one texture transform per material, whereas
+    //      KHR_texture_transform supports one texture transform per texture info.
+    //      i.e. this isn't fully compliant with KHR_texture_transform, but is
+    //      compliant when all texture infos used by a material have the same
+    //      texture transform.
+
+    LLGLTFMaterial::TextureTransform::PackTight transforms_packed[terrain_material_count];
+    for (U32 i = 0; i < terrain_material_count; ++i)
+	{
+		const LLFetchedGLTFMaterial* fetched_material = (*fetched_materials)[i].get();
+        LLGLTFMaterial::TextureTransform transform;
+        if (fetched_material)
+        {
+            transform = fetched_material->mTextureTransform[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR];
+#ifdef SHOW_ASSERT
+            // Assert condition where the contents of the texture transforms
+            // differ per texture info - we currently don't support this case.
+            for (U32 ti = 1; ti < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++ti)
+            {
+                llassert(fetched_material->mTextureTransform[0] == fetched_material->mTextureTransform[ti]);
+            }
+#endif
+        }
+        // *NOTE: Notice here we are combining the scale from
+        // RenderTerrainPBRScale into the KHR_texture_transform. This only
+        // works if the scale is uniform and no other transforms are
+        // applied to the terrain UVs.
+        transform.mScale.mV[VX] *= sPBRDetailScale;
+        transform.mScale.mV[VY] *= sPBRDetailScale;
+
+        transform.getPackedTight(transforms_packed[i]);
+    }
+    const U32 transform_param_count = LLGLTFMaterial::TextureTransform::PACK_TIGHT_SIZE * terrain_material_count;
+    constexpr U32 vec4_size = 4;
+    const U32 transform_vec4_count = (transform_param_count + (vec4_size - 1)) / vec4_size;
+    llassert(transform_vec4_count == 5); // If false, need to update shader
+    shader->uniform4fv(LLShaderMgr::TERRAIN_TEXTURE_TRANSFORMS, transform_vec4_count, (F32*)transforms_packed);
 
     LLSettingsWater::ptr_t pwater = LLEnvironment::instance().getCurrentWater();
 
diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp
index 4aadc822ab..410d6a9d55 100644
--- a/indra/newview/llviewercontrol.cpp
+++ b/indra/newview/llviewercontrol.cpp
@@ -691,6 +691,28 @@ void handleLocalTerrainChanged(const LLSD& newValue)
         const auto setting = gSavedSettings.getString(std::string("LocalTerrainAsset") + std::to_string(i + 1));
         const LLUUID materialID(setting);
         gLocalTerrainMaterials.setDetailAssetID(i, materialID);
+
+        // *NOTE: The GLTF spec allows for different texture infos to have their texture transforms set independently, but as a simplification, this debug setting only updates all the transforms in-sync (i.e. only one texture transform per terrain material).
+        LLGLTFMaterial::TextureTransform transform;
+        const std::string prefix = std::string("LocalTerrainTransform") + std::to_string(i + 1);
+        transform.mScale.mV[VX] = gSavedSettings.getF32(prefix + "ScaleU");
+        transform.mScale.mV[VY] = gSavedSettings.getF32(prefix + "ScaleV");
+        transform.mRotation = gSavedSettings.getF32(prefix + "Rotation") * DEG_TO_RAD;
+        transform.mOffset.mV[VX] = gSavedSettings.getF32(prefix + "OffsetU");
+        transform.mOffset.mV[VY] = gSavedSettings.getF32(prefix + "OffsetV");
+        LLPointer<LLGLTFMaterial> mat_override = new LLGLTFMaterial();
+        for (U32 info = 0; info < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++info)
+        {
+            mat_override->mTextureTransform[info] = transform;
+        }
+        if (*mat_override == LLGLTFMaterial::sDefault)
+        {
+            gLocalTerrainMaterials.setMaterialOverride(i, nullptr);
+        }
+        else
+        {
+            gLocalTerrainMaterials.setMaterialOverride(i, mat_override);
+		}
     }
 }
 ////////////////////////////////////////////////////////////////////////////
@@ -879,10 +901,25 @@ void settings_setup_listeners()
     setting_setup_signal_listener(gSavedSettings, "AutoTuneImpostorFarAwayDistance", handleUserImpostorDistanceChanged);
     setting_setup_signal_listener(gSavedSettings, "AutoTuneImpostorByDistEnabled", handleUserImpostorByDistEnabledChanged);
     setting_setup_signal_listener(gSavedSettings, "TuningFPSStrategy", handleFPSTuningStrategyChanged);
-    setting_setup_signal_listener(gSavedSettings, "LocalTerrainAsset1", handleLocalTerrainChanged);
-    setting_setup_signal_listener(gSavedSettings, "LocalTerrainAsset2", handleLocalTerrainChanged);
-    setting_setup_signal_listener(gSavedSettings, "LocalTerrainAsset3", handleLocalTerrainChanged);
-    setting_setup_signal_listener(gSavedSettings, "LocalTerrainAsset4", handleLocalTerrainChanged);
+    {
+        const char* transform_suffixes[] = {
+            "ScaleU",
+            "ScaleV",
+            "Rotation",
+            "OffsetU",
+            "OffsetV"
+        };
+        for (U32 i = 0; i < LLTerrainMaterials::ASSET_COUNT; ++i)
+        {
+            const auto asset_setting_name = std::string("LocalTerrainAsset") + std::to_string(i + 1);
+            setting_setup_signal_listener(gSavedSettings, asset_setting_name, handleLocalTerrainChanged);
+            for (const char* ts : transform_suffixes)
+            {
+                const auto transform_setting_name = std::string("LocalTerrainTransform") + std::to_string(i + 1) + ts;
+                setting_setup_signal_listener(gSavedSettings, transform_setting_name, handleLocalTerrainChanged);
+            }
+        }
+    }
 
     setting_setup_signal_listener(gSavedPerAccountSettings, "AvatarHoverOffsetZ", handleAvatarHoverOffsetChanged);
 }
diff --git a/indra/newview/llvlcomposition.cpp b/indra/newview/llvlcomposition.cpp
index dc97beac93..c161c42256 100644
--- a/indra/newview/llvlcomposition.cpp
+++ b/indra/newview/llvlcomposition.cpp
@@ -188,9 +188,24 @@ void LLTerrainMaterials::setDetailAssetID(S32 asset, const LLUUID& id)
 	mDetailTextures[asset] = fetch_terrain_texture(id);
     LLPointer<LLFetchedGLTFMaterial>& mat = mDetailMaterials[asset];
     mat = id.isNull() ? nullptr : gGLTFMaterialList.getMaterial(id);
+    mDetailRenderMaterials[asset] = nullptr;
     mMaterialTexturesSet[asset] = false;
 }
 
+const LLGLTFMaterial* LLTerrainMaterials::getMaterialOverride(S32 asset)
+{
+    return mDetailMaterialOverrides[asset];
+}
+
+void LLTerrainMaterials::setMaterialOverride(S32 asset, LLGLTFMaterial* mat_override)
+{
+    // Non-null overrides must be nontrivial. Otherwise, please set the override to null instead.
+    llassert(!mat_override || *mat_override != LLGLTFMaterial::sDefault);
+
+    mDetailMaterialOverrides[asset] = mat_override;
+    mDetailRenderMaterials[asset] = nullptr;
+}
+
 LLTerrainMaterials::Type LLTerrainMaterials::getMaterialType()
 {
 	LL_PROFILE_ZONE_SCOPED;
@@ -221,13 +236,36 @@ bool LLTerrainMaterials::texturesReady(bool boost, bool strict)
     return one_ready;
 }
 
+namespace
+{
+    bool material_asset_ready(LLFetchedGLTFMaterial* mat) { return mat && mat->isLoaded(); }
+};
+
 bool LLTerrainMaterials::materialsReady(bool boost, bool strict)
 {
     bool ready[ASSET_COUNT];
-    // *NOTE: Calls to materialReady may boost materials/textures. Do not early-return.
+    // *NOTE: This section may boost materials/textures. Do not early-return if ready[i] is false.
     for (S32 i = 0; i < ASSET_COUNT; i++)
     {
-        ready[i] = materialReady(mDetailMaterials[i], mMaterialTexturesSet[i], boost, strict);
+        ready[i] = false;
+        LLPointer<LLFetchedGLTFMaterial>& mat = mDetailMaterials[i];
+        if (!material_asset_ready(mat)) { continue; }
+
+        LLPointer<LLFetchedGLTFMaterial>& render_mat = mDetailRenderMaterials[i];
+        if (!render_mat)
+        {
+            render_mat = new LLFetchedGLTFMaterial();
+            *render_mat = *mat;
+            // This render_mat is effectively already loaded, because it gets its data from mat.
+
+            LLPointer<LLGLTFMaterial>& override_mat = mDetailMaterialOverrides[i];
+            if (override_mat)
+            {
+                render_mat->applyOverride(*override_mat);
+            }
+        }
+
+        ready[i] = materialTexturesReady(render_mat, mMaterialTexturesSet[i], boost, strict);
     }
 
 #if 1
@@ -308,15 +346,13 @@ bool LLTerrainMaterials::textureReady(LLPointer<LLViewerFetchedTexture>& tex, bo
     return true;
 }
 
-// Boost the loading priority of every known texture in the material
-// Return true when ready to use
+// Make sure to call material_asset_ready first
+// strict = true -> all materials must be sufficiently loaded
+// strict = false -> at least one material must be loaded
 // static
-bool LLTerrainMaterials::materialReady(LLPointer<LLFetchedGLTFMaterial> &mat, bool &textures_set, bool boost, bool strict)
+bool LLTerrainMaterials::materialTexturesReady(LLPointer<LLFetchedGLTFMaterial>& mat, bool& textures_set, bool boost, bool strict)
 {
-    if (!mat || !mat->isLoaded())
-    {
-        return false;
-    }
+    llassert(mat);
 
     // Material is loaded, but textures may not be
     if (!textures_set)
@@ -357,6 +393,16 @@ bool LLTerrainMaterials::materialReady(LLPointer<LLFetchedGLTFMaterial> &mat, bo
     return true;
 }
 
+// Boost the loading priority of every known texture in the material
+// Return true when ready to use
+// static
+bool LLTerrainMaterials::materialReady(LLPointer<LLFetchedGLTFMaterial> &mat, bool &textures_set, bool boost, bool strict)
+{
+    if (!material_asset_ready(mat)) { return false; }
+
+    return materialTexturesReady(mat, textures_set, boost, strict);
+}
+
 // static
 const LLUUID (&LLVLComposition::getDefaultTextures())[ASSET_COUNT]
 {
@@ -669,19 +715,20 @@ bool LLVLComposition::generateMinimapTileLand(const F32 x, const F32 y,
             }
             else
             {
-                tex = mDetailMaterials[i]->mBaseColorTexture;
-                tex_emissive = mDetailMaterials[i]->mEmissiveTexture;
-                base_color_factor = LLColor3(mDetailMaterials[i]->mBaseColor);
+                LLPointer<LLFetchedGLTFMaterial>& mat = mDetailRenderMaterials[i];
+                tex = mat->mBaseColorTexture;
+                tex_emissive = mat->mEmissiveTexture;
+                base_color_factor = LLColor3(mat->mBaseColor);
                 // *HACK: Treat alpha as black
-                base_color_factor *= (mDetailMaterials[i]->mBaseColor.mV[VW]);
-                emissive_factor = mDetailMaterials[i]->mEmissiveColor;
+                base_color_factor *= (mat->mBaseColor.mV[VW]);
+                emissive_factor = mat->mEmissiveColor;
                 has_base_color_factor = (base_color_factor.mV[VX] != 1.f ||
                                          base_color_factor.mV[VY] != 1.f ||
                                          base_color_factor.mV[VZ] != 1.f);
                 has_emissive_factor = (emissive_factor.mV[VX] != 1.f ||
                                        emissive_factor.mV[VY] != 1.f ||
                                        emissive_factor.mV[VZ] != 1.f);
-                has_alpha = mDetailMaterials[i]->mAlphaMode != LLGLTFMaterial::ALPHA_MODE_OPAQUE;
+                has_alpha = mat->mAlphaMode != LLGLTFMaterial::ALPHA_MODE_OPAQUE;
             }
 
             if (!tex) { tex = LLViewerFetchedTexture::sWhiteImagep; }
diff --git a/indra/newview/llvlcomposition.h b/indra/newview/llvlcomposition.h
index cde0e216c3..568786973d 100644
--- a/indra/newview/llvlcomposition.h
+++ b/indra/newview/llvlcomposition.h
@@ -35,6 +35,7 @@
 class LLSurface;
 
 class LLViewerFetchedTexture;
+class LLGLTFMaterial;
 class LLFetchedGLTFMaterial;
 
 class LLTerrainMaterials
@@ -62,6 +63,8 @@ public:
 
 	virtual LLUUID getDetailAssetID(S32 asset);
 	virtual void setDetailAssetID(S32 asset, const LLUUID& id);
+	virtual const LLGLTFMaterial* getMaterialOverride(S32 asset);
+	virtual void setMaterialOverride(S32 asset, LLGLTFMaterial* mat_override);
     Type getMaterialType();
     bool texturesReady(bool boost, bool strict);
     // strict = true -> all materials must be sufficiently loaded
@@ -74,8 +77,13 @@ protected:
     // strict = true -> all materials must be sufficiently loaded
     // strict = false -> at least one material must be loaded
     static bool materialReady(LLPointer<LLFetchedGLTFMaterial>& mat, bool& textures_set, bool boost, bool strict);
+    // *NOTE: Prefer calling materialReady if mat is known to be LLFetchedGLTFMaterial
+    static bool materialTexturesReady(LLPointer<LLFetchedGLTFMaterial>& mat, bool& textures_set, bool boost, bool strict);
+
 	LLPointer<LLViewerFetchedTexture> mDetailTextures[ASSET_COUNT];
 	LLPointer<LLFetchedGLTFMaterial> mDetailMaterials[ASSET_COUNT];
+    LLPointer<LLGLTFMaterial> mDetailMaterialOverrides[ASSET_COUNT];
+    LLPointer<LLFetchedGLTFMaterial> mDetailRenderMaterials[ASSET_COUNT];
     bool mMaterialTexturesSet[ASSET_COUNT];
 };
 
@@ -125,9 +133,6 @@ public:
 	bool getParamsReady() const	{ return mParamsReady; }
 
 protected:
-    static bool textureReady(LLPointer<LLViewerFetchedTexture>& tex, bool boost = false);
-    static bool materialReady(LLPointer<LLFetchedGLTFMaterial>& mat, bool& textures_set, bool boost = false);
-
 	bool mParamsReady = false;
 	LLSurface *mSurfacep;
 
-- 
cgit v1.2.3