diff --git a/libraries/README.md b/libraries/README.md
index 40842a0e28..a1a868798b 100644
--- a/libraries/README.md
+++ b/libraries/README.md
@@ -75,5 +75,4 @@ This folder contains the standard data libraries for MaterialX, providing declar
- point, directional, spot
- Shader generation does not currently support:
- `displacementshader` and `volumeshader` nodes for hardware shading targets (GLSL, MSL).
- - `hextiledimage` and `hextilednormalmap` for OSL and MDL.
- `blur` the implementation passes through `in` unmodified in all shading languages.
diff --git a/libraries/stdlib/genosl/lib/mx_hextile.osl b/libraries/stdlib/genosl/lib/mx_hextile.osl
new file mode 100644
index 0000000000..ab064f28de
--- /dev/null
+++ b/libraries/stdlib/genosl/lib/mx_hextile.osl
@@ -0,0 +1,176 @@
+// https://www.shadertoy.com/view/4djSRW
+vector2 mx_hextile_hash(vector2 p)
+{
+ vector pp3 = fmod(vector(p.x, p.y, p.x) * vector(0.1031, 0.1030, 0.0973), 1.0);
+ pp3 += dot(pp3, vector(pp3[1], pp3[2], pp3[0]) + 33.33);
+ return fmod(vector2(pp3[0] + pp3[1], pp3[0] + pp3[2]) * vector2(pp3[2], pp3[1]), 1.0);
+}
+
+// Christophe Schlick. "Fast Alternatives to Perlin's Bias and Gain Functions".
+// In Graphics Gems IV, Morgan Kaufmann, 1994, pages 401-403.
+// https://dept-info.labri.fr/~schlick/DOC/gem2.html
+float mx_schlick_gain(float x, float r)
+{
+ float rr = clamp(r, 0.001, 0.999); // to avoid glitch
+ float a = (1.0 / rr - 2.0) * (1.0 - 2.0 * x);
+ return (x < 0.5) ? x / (a + 1.0) : (a - x) / (a - 1.0);
+}
+
+struct HextileData
+{
+ vector2 coords[3];
+ vector weights;
+ vector rotations;
+};
+
+// Helper function to compute blend weights with optional falloff
+vector mx_hextile_compute_blend_weights(vector luminance_weights, vector tile_weights, float falloff)
+{
+ vector w = luminance_weights * pow(tile_weights, 7.0);
+ w /= (w[0] + w[1] + w[2]);
+
+ if (falloff != 0.5)
+ {
+ w[0] = mx_schlick_gain(w[0], falloff);
+ w[1] = mx_schlick_gain(w[1], falloff);
+ w[2] = mx_schlick_gain(w[2], falloff);
+ w /= (w[0] + w[1] + w[2]);
+ }
+ return w;
+}
+
+// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
+// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
+// http://jcgt.org/published/0011/03/05/
+HextileData mx_hextile_coord(
+ vector2 coord,
+ float rotation,
+ vector2 rotation_range,
+ float scale,
+ vector2 scale_range,
+ float offset,
+ vector2 offset_range)
+{
+ float sqrt3_2 = sqrt(3.0) * 2.0;
+
+ // Scale coord to maintain the original fit
+ vector2 st = coord * sqrt3_2;
+
+ // Skew input space into simplex triangle grid
+ // (1, 0, -tan(30), 2*tan(30))
+ vector2 st_skewed = vector2(st.x + st.y * -0.57735027, st.y * 1.15470054);
+
+ // Barycentric weights
+ vector2 st_frac = fmod(st_skewed, 1.0);
+ // Handle negative coordinates properly
+ if (st_frac.x < 0.0) st_frac.x += 1.0;
+ if (st_frac.y < 0.0) st_frac.y += 1.0;
+
+ vector temp = vector(st_frac.x, st_frac.y, 0.0);
+ temp[2] = 1.0 - temp[0] - temp[1];
+
+ float s = step(0.0, -temp[2]);
+ float s2 = 2.0 * s - 1.0;
+
+ float w1 = -temp[2] * s2;
+ float w2 = s - temp[1] * s2;
+ float w3 = s - temp[0] * s2;
+
+ // Vertex IDs
+ int base_id_x = int(floor(st_skewed.x));
+ int base_id_y = int(floor(st_skewed.y));
+ int si = int(s);
+ int id1_x = base_id_x + si;
+ int id1_y = base_id_y + si;
+ int id2_x = base_id_x + si;
+ int id2_y = base_id_y + 1 - si;
+ int id3_x = base_id_x + 1 - si;
+ int id3_y = base_id_y + si;
+
+ // Vertex IDs as array for looping
+ int id_x[3] = { id1_x, id2_x, id3_x };
+ int id_y[3] = { id1_y, id2_y, id3_y };
+
+ // Tile centers, random values, and offsets
+ vector2 ctr[3], rand[3], offsets[3];
+ vector2 seed_offset = vector2(0.12345, 0.12345); // To avoid some zeros
+ vector2 rr = vector2(radians(rotation_range.x), radians(rotation_range.y));
+ vector rotations, scales;
+
+ for (int i = 0; i < 3; i++)
+ {
+ ctr[i] = vector2(float(id_x[i]) + float(id_y[i]) * 0.5, float(id_y[i]) / 1.15470054) / sqrt3_2;
+ rand[i] = mx_hextile_hash(vector2(float(id_x[i]), float(id_y[i])) + seed_offset);
+ rotations[i] = mix(rr.x, rr.y, rand[i].x * rotation);
+ scales[i] = mix(1.0, mix(scale_range.x, scale_range.y, rand[i].y), scale);
+ offsets[i] = vector2(
+ mix(offset_range.x, offset_range.y, rand[i].x * offset),
+ mix(offset_range.x, offset_range.y, rand[i].y * offset));
+ }
+
+ vector sin_r = sin(rotations);
+ vector cos_r = cos(rotations);
+
+ HextileData tile_data;
+ tile_data.weights = vector(w1, w2, w3);
+ tile_data.rotations = rotations;
+
+ // Transform coordinates for each tile
+ for (int i = 0; i < 3; i++)
+ {
+ vector2 d = coord - ctr[i];
+ float c = cos_r[i];
+ float s = sin_r[i];
+ float sc = scales[i];
+
+ tile_data.coords[i] = vector2(
+ (d.x * c - d.y * s) / sc + ctr[i].x + offsets[i].x,
+ (d.x * s + d.y * c) / sc + ctr[i].y + offsets[i].y);
+ }
+
+ return tile_data;
+}
+
+// Blend 3 normals by blending the gradients
+// Morten S. Mikkelsen, Surface Gradient-Based Bump Mapping Framework, Journal of
+// Computer Graphics Techniques (JCGT), vol. 9, no. 3, 60-90, 2020
+// http://jcgt.org/published/0009/03/04/
+vector mx_normals_to_gradient(vector N, vector Np)
+{
+ float d = dot(N, Np);
+ vector g = (d * N - Np) / max(1e-8, abs(d));
+ return g;
+}
+
+vector mx_gradient_blend_3_normals(vector N, vector N1, float N1_weight, vector N2, float N2_weight, vector N3, float N3_weight)
+{
+ float w1 = clamp(N1_weight, 0.0, 1.0);
+ float w2 = clamp(N2_weight, 0.0, 1.0);
+ float w3 = clamp(N3_weight, 0.0, 1.0);
+
+ vector g1 = mx_normals_to_gradient(N, N1);
+ vector g2 = mx_normals_to_gradient(N, N2);
+ vector g3 = mx_normals_to_gradient(N, N3);
+
+ // Blend
+ vector gg = w1 * g1 + w2 * g2 + w3 * g3;
+
+ // Gradient to normal
+ return normalize(N - gg);
+}
+
+// Axis-angle rotation matrix
+matrix mx_axis_rotation_matrix(vector axis, float angle)
+{
+ float s = sin(angle);
+ float c = cos(angle);
+ float omc = 1.0 - c;
+ vector a = axis;
+
+ return matrix(
+ a[0]*a[0]*omc + c, a[0]*a[1]*omc - a[2]*s, a[0]*a[2]*omc + a[1]*s, 0.0,
+ a[1]*a[0]*omc + a[2]*s, a[1]*a[1]*omc + c, a[1]*a[2]*omc - a[0]*s, 0.0,
+ a[2]*a[0]*omc - a[1]*s, a[2]*a[1]*omc + a[0]*s, a[2]*a[2]*omc + c, 0.0,
+ 0.0, 0.0, 0.0, 1.0
+ );
+}
diff --git a/libraries/stdlib/genosl/mx_hextiledimage_color3.osl b/libraries/stdlib/genosl/mx_hextiledimage_color3.osl
new file mode 100644
index 0000000000..d052f65f36
--- /dev/null
+++ b/libraries/stdlib/genosl/mx_hextiledimage_color3.osl
@@ -0,0 +1,55 @@
+#include "lib/$fileTransformUv"
+#include "lib/mx_hextile.osl"
+
+// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
+// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
+// http://jcgt.org/published/0011/03/05/
+void mx_hextiledimage_color3(
+ textureresource file,
+ color default_value,
+ vector2 texcoord,
+ vector2 tiling,
+ float rotation,
+ vector2 rotationrange,
+ float scale,
+ vector2 scalerange,
+ float offset,
+ vector2 offsetrange,
+ float falloff,
+ float falloffcontrast,
+ color lumacoeffs,
+ output color result)
+{
+ if (file.filename == "")
+ {
+ result = default_value;
+ return;
+ }
+
+ vector2 coord = mx_transform_uv(texcoord) * tiling;
+
+ HextileData tile_data = mx_hextile_coord(coord, rotation, rotationrange, scale, scalerange, offset, offsetrange);
+
+ // Sample textures for each tile
+ color c[3];
+ vector cw;
+
+ for (int i = 0; i < 3; i++)
+ {
+ c[i] = texture(file.filename, tile_data.coords[i].x, tile_data.coords[i].y,
+ "missingcolor", default_value,
+ "swrap", "periodic", "twrap", "periodic"
+#if (OSL_VERSION_MAJOR == 1 && OSL_VERSION_MINOR >= 14) || (OSL_VERSION_MAJOR > 1)
+ , "colorspace", file.colorspace
+#endif
+ );
+ cw[i] = dot(vector(c[i]), vector(lumacoeffs));
+ }
+
+ // Luminance-based blend weights
+ cw = mix(vector(1.0), cw, vector(falloffcontrast));
+ vector w = mx_hextile_compute_blend_weights(cw, tile_data.weights, falloff);
+
+ // Blend colors
+ result = w[0] * c[0] + w[1] * c[1] + w[2] * c[2];
+}
diff --git a/libraries/stdlib/genosl/mx_hextiledimage_color4.osl b/libraries/stdlib/genosl/mx_hextiledimage_color4.osl
new file mode 100644
index 0000000000..cb273fb3c9
--- /dev/null
+++ b/libraries/stdlib/genosl/mx_hextiledimage_color4.osl
@@ -0,0 +1,65 @@
+#include "lib/$fileTransformUv"
+#include "lib/mx_hextile.osl"
+
+// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
+// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
+// http://jcgt.org/published/0011/03/05/
+void mx_hextiledimage_color4(
+ textureresource file,
+ color4 default_value,
+ vector2 texcoord,
+ vector2 tiling,
+ float rotation,
+ vector2 rotationrange,
+ float scale,
+ vector2 scalerange,
+ float offset,
+ vector2 offsetrange,
+ float falloff,
+ float falloffcontrast,
+ color lumacoeffs,
+ output color4 result)
+{
+ if (file.filename == "")
+ {
+ result = default_value;
+ return;
+ }
+
+ vector2 coord = mx_transform_uv(texcoord) * tiling;
+
+ HextileData tile_data = mx_hextile_coord(coord, rotation, rotationrange, scale, scalerange, offset, offsetrange);
+
+ // Sample textures for each tile
+ color c[3];
+ float alpha[3];
+ vector cw;
+
+ for (int i = 0; i < 3; i++)
+ {
+ c[i] = texture(file.filename, tile_data.coords[i].x, tile_data.coords[i].y,
+ "alpha", alpha[i],
+ "missingcolor", default_value.rgb, "missingalpha", default_value.a,
+ "swrap", "periodic", "twrap", "periodic"
+#if (OSL_VERSION_MAJOR == 1 && OSL_VERSION_MINOR >= 14) || (OSL_VERSION_MAJOR > 1)
+ , "colorspace", file.colorspace
+#endif
+ );
+ cw[i] = dot(vector(c[i]), vector(lumacoeffs));
+ }
+
+ // Luminance-based blend weights
+ cw = mix(vector(1.0), cw, vector(falloffcontrast));
+ vector w = mx_hextile_compute_blend_weights(cw, tile_data.weights, falloff);
+
+ // Alpha (average with optional falloff adjustment)
+ float a = (alpha[0] + alpha[1] + alpha[2]) / 3.0;
+ if (falloff != 0.5)
+ {
+ a = mx_schlick_gain(a, falloff);
+ }
+
+ // Blend colors
+ result.rgb = w[0] * c[0] + w[1] * c[1] + w[2] * c[2];
+ result.a = a;
+}
diff --git a/libraries/stdlib/genosl/mx_hextilednormalmap_vector3.osl b/libraries/stdlib/genosl/mx_hextilednormalmap_vector3.osl
new file mode 100644
index 0000000000..266287663a
--- /dev/null
+++ b/libraries/stdlib/genosl/mx_hextilednormalmap_vector3.osl
@@ -0,0 +1,68 @@
+#include "lib/$fileTransformUv"
+#include "lib/mx_hextile.osl"
+
+// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
+// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
+// http://jcgt.org/published/0011/03/05/
+void mx_hextilednormalmap_vector3(
+ textureresource file,
+ vector default_value,
+ vector2 texcoord,
+ vector2 tiling,
+ float rotation,
+ vector2 rotationrange,
+ float scale,
+ vector2 scalerange,
+ float offset,
+ vector2 offsetrange,
+ float falloff,
+ float strength,
+ int flip_g,
+ vector N,
+ vector T,
+ vector B,
+ output vector result)
+{
+ if (file.filename == "")
+ {
+ result = N;
+ return;
+ }
+
+ vector2 coord = mx_transform_uv(texcoord) * tiling;
+
+ HextileData tile_data = mx_hextile_coord(coord, rotation, rotationrange, scale, scalerange, offset, offsetrange);
+
+ // Process each tile: sample, decode, transform, and compute normals
+ vector tile_normals[3];
+
+ for (int i = 0; i < 3; i++)
+ {
+ // Sample normal map for this tile
+ color nm_raw = texture(file.filename, tile_data.coords[i].x, tile_data.coords[i].y,
+ "missingcolor", color(default_value),
+ "swrap", "periodic", "twrap", "periodic");
+
+ // Convert to vector and decode normal map
+ vector nm = vector(nm_raw);
+ if (flip_g)
+ nm[1] = 1.0 - nm[1];
+ nm = 2.0 * nm - 1.0;
+
+ // Rotate tangent frame for this tile
+ matrix tangent_rot_mat = mx_axis_rotation_matrix(N, -tile_data.rotations[i]);
+ vector Ti = transform(tangent_rot_mat, T) * strength;
+ vector Bi = transform(tangent_rot_mat, B) * strength;
+
+ // The OSL backend uses dPdu and dPdv for tangents and bitangents, but these vectors are not
+ // guaranteed to be orthonormal. Orthogonalize the tangent frame using Gram-Schmidt.
+ vector Tn = normalize(Ti - dot(Ti, N) * N);
+ vector Bn = normalize(Bi - dot(Bi, N) * N - dot(Bi, Tn) * Tn);
+
+ tile_normals[i] = normalize(Tn * nm[0] + Bn * nm[1] + N * nm[2]);
+ }
+
+ // Blend weights and normals using gradient blending
+ vector w = mx_hextile_compute_blend_weights(vector(1.0), tile_data.weights, falloff);
+ result = mx_gradient_blend_3_normals(N, tile_normals[0], w[0], tile_normals[1], w[1], tile_normals[2], w[2]);
+}
diff --git a/libraries/stdlib/genosl/stdlib_genosl_impl.mtlx b/libraries/stdlib/genosl/stdlib_genosl_impl.mtlx
index b909c5e89a..80f21aca74 100644
--- a/libraries/stdlib/genosl/stdlib_genosl_impl.mtlx
+++ b/libraries/stdlib/genosl/stdlib_genosl_impl.mtlx
@@ -67,12 +67,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/Materials/TestSuite/pbrlib/surfaceshader/standard_surface_onyx_hextiled.mtlx b/resources/Materials/Examples/StandardSurface/standard_surface_onyx_hextiled.mtlx
similarity index 97%
rename from resources/Materials/TestSuite/pbrlib/surfaceshader/standard_surface_onyx_hextiled.mtlx
rename to resources/Materials/Examples/StandardSurface/standard_surface_onyx_hextiled.mtlx
index 19b08a27c3..72d7098849 100644
--- a/resources/Materials/TestSuite/pbrlib/surfaceshader/standard_surface_onyx_hextiled.mtlx
+++ b/resources/Materials/Examples/StandardSurface/standard_surface_onyx_hextiled.mtlx
@@ -1,6 +1,6 @@
-
+
diff --git a/source/MaterialXTest/MaterialXGenOsl/GenOsl.cpp b/source/MaterialXTest/MaterialXGenOsl/GenOsl.cpp
index c56930d01b..deb9ddd45c 100644
--- a/source/MaterialXTest/MaterialXGenOsl/GenOsl.cpp
+++ b/source/MaterialXTest/MaterialXGenOsl/GenOsl.cpp
@@ -90,10 +90,6 @@ TEST_CASE("GenShader: OSL Implementation Check", "[genosl]")
generatorSkipNodeTypes.insert("light");
mx::StringSet generatorSkipNodeDefs;
- generatorSkipNodeDefs.insert("ND_hextiledimage_color3");
- generatorSkipNodeDefs.insert("ND_hextiledimage_color4");
- generatorSkipNodeDefs.insert("ND_hextilednormalmap_vector3");
-
GenShaderUtil::checkImplementations(context, generatorSkipNodeTypes, generatorSkipNodeDefs);
}
diff --git a/source/MaterialXTest/MaterialXGenOsl/GenOsl.h b/source/MaterialXTest/MaterialXGenOsl/GenOsl.h
index 634abb8b60..cf704da805 100644
--- a/source/MaterialXTest/MaterialXGenOsl/GenOsl.h
+++ b/source/MaterialXTest/MaterialXGenOsl/GenOsl.h
@@ -45,8 +45,8 @@ class OslShaderGeneratorTester : public GenShaderUtil::ShaderGeneratorTester
void addSkipFiles() override
{
- _skipFiles.insert("standard_surface_onyx_hextiled.mtlx");
- _skipFiles.insert("hextiled.mtlx");
+ // No additional files are skipped
+ ShaderGeneratorTester::addSkipFiles();
}
// Ignore light shaders in the document for OSL
diff --git a/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp b/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp
index 42ab9aee9c..c8b8d45bbb 100644
--- a/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp
+++ b/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp
@@ -105,10 +105,8 @@ class OslShaderRenderTester : public RenderUtil::ShaderRenderTester
void addSkipFiles() override
{
- _skipFiles.insert("standard_surface_onyx_hextiled.mtlx");
if (_useOslCmdStr)
{
- _skipFiles.insert("hextiled.mtlx");
_skipFiles.insert("filename_cm_test.mtlx");
_skipFiles.insert("shader_ops.mtlx");
_skipFiles.insert("chiang_hair_surfaceshader.mtlx");