Skip to content

Commit aefa3f5

Browse files
Add OSL hextiling implementations (#2734)
This changelist adds implementations of `hextiledimage` and `hextilednormalmap` to OSL, making these nodes available across all shading languages that MaterialX supports. The new OSL implementations closely follow the existing GLSL versions, with minor improvements to take better advantage of OSL channel indexing and loops. Additionally, the `standard_surface_onyx_hextiled.mtlx` example has been moved to `Examples/StandardSurface`, making it more visible to users of the MaterialX Web Viewer and other tools.
1 parent b9a10ed commit aefa3f5

10 files changed

Lines changed: 380 additions & 10 deletions

File tree

libraries/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,4 @@ This folder contains the standard data libraries for MaterialX, providing declar
7575
- point, directional, spot
7676
- Shader generation does not currently support:
7777
- `displacementshader` and `volumeshader` nodes for hardware shading targets (GLSL, MSL).
78-
- `hextiledimage` and `hextilednormalmap` for OSL and MDL.
7978
- `blur` the implementation passes through `in` unmodified in all shading languages.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// https://www.shadertoy.com/view/4djSRW
2+
vector2 mx_hextile_hash(vector2 p)
3+
{
4+
vector pp3 = fmod(vector(p.x, p.y, p.x) * vector(0.1031, 0.1030, 0.0973), 1.0);
5+
pp3 += dot(pp3, vector(pp3[1], pp3[2], pp3[0]) + 33.33);
6+
return fmod(vector2(pp3[0] + pp3[1], pp3[0] + pp3[2]) * vector2(pp3[2], pp3[1]), 1.0);
7+
}
8+
9+
// Christophe Schlick. "Fast Alternatives to Perlin's Bias and Gain Functions".
10+
// In Graphics Gems IV, Morgan Kaufmann, 1994, pages 401-403.
11+
// https://dept-info.labri.fr/~schlick/DOC/gem2.html
12+
float mx_schlick_gain(float x, float r)
13+
{
14+
float rr = clamp(r, 0.001, 0.999); // to avoid glitch
15+
float a = (1.0 / rr - 2.0) * (1.0 - 2.0 * x);
16+
return (x < 0.5) ? x / (a + 1.0) : (a - x) / (a - 1.0);
17+
}
18+
19+
struct HextileData
20+
{
21+
vector2 coords[3];
22+
vector weights;
23+
vector rotations;
24+
};
25+
26+
// Helper function to compute blend weights with optional falloff
27+
vector mx_hextile_compute_blend_weights(vector luminance_weights, vector tile_weights, float falloff)
28+
{
29+
vector w = luminance_weights * pow(tile_weights, 7.0);
30+
w /= (w[0] + w[1] + w[2]);
31+
32+
if (falloff != 0.5)
33+
{
34+
w[0] = mx_schlick_gain(w[0], falloff);
35+
w[1] = mx_schlick_gain(w[1], falloff);
36+
w[2] = mx_schlick_gain(w[2], falloff);
37+
w /= (w[0] + w[1] + w[2]);
38+
}
39+
return w;
40+
}
41+
42+
// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
43+
// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
44+
// http://jcgt.org/published/0011/03/05/
45+
HextileData mx_hextile_coord(
46+
vector2 coord,
47+
float rotation,
48+
vector2 rotation_range,
49+
float scale,
50+
vector2 scale_range,
51+
float offset,
52+
vector2 offset_range)
53+
{
54+
float sqrt3_2 = sqrt(3.0) * 2.0;
55+
56+
// Scale coord to maintain the original fit
57+
vector2 st = coord * sqrt3_2;
58+
59+
// Skew input space into simplex triangle grid
60+
// (1, 0, -tan(30), 2*tan(30))
61+
vector2 st_skewed = vector2(st.x + st.y * -0.57735027, st.y * 1.15470054);
62+
63+
// Barycentric weights
64+
vector2 st_frac = fmod(st_skewed, 1.0);
65+
// Handle negative coordinates properly
66+
if (st_frac.x < 0.0) st_frac.x += 1.0;
67+
if (st_frac.y < 0.0) st_frac.y += 1.0;
68+
69+
vector temp = vector(st_frac.x, st_frac.y, 0.0);
70+
temp[2] = 1.0 - temp[0] - temp[1];
71+
72+
float s = step(0.0, -temp[2]);
73+
float s2 = 2.0 * s - 1.0;
74+
75+
float w1 = -temp[2] * s2;
76+
float w2 = s - temp[1] * s2;
77+
float w3 = s - temp[0] * s2;
78+
79+
// Vertex IDs
80+
int base_id_x = int(floor(st_skewed.x));
81+
int base_id_y = int(floor(st_skewed.y));
82+
int si = int(s);
83+
int id1_x = base_id_x + si;
84+
int id1_y = base_id_y + si;
85+
int id2_x = base_id_x + si;
86+
int id2_y = base_id_y + 1 - si;
87+
int id3_x = base_id_x + 1 - si;
88+
int id3_y = base_id_y + si;
89+
90+
// Vertex IDs as array for looping
91+
int id_x[3] = { id1_x, id2_x, id3_x };
92+
int id_y[3] = { id1_y, id2_y, id3_y };
93+
94+
// Tile centers, random values, and offsets
95+
vector2 ctr[3], rand[3], offsets[3];
96+
vector2 seed_offset = vector2(0.12345, 0.12345); // To avoid some zeros
97+
vector2 rr = vector2(radians(rotation_range.x), radians(rotation_range.y));
98+
vector rotations, scales;
99+
100+
for (int i = 0; i < 3; i++)
101+
{
102+
ctr[i] = vector2(float(id_x[i]) + float(id_y[i]) * 0.5, float(id_y[i]) / 1.15470054) / sqrt3_2;
103+
rand[i] = mx_hextile_hash(vector2(float(id_x[i]), float(id_y[i])) + seed_offset);
104+
rotations[i] = mix(rr.x, rr.y, rand[i].x * rotation);
105+
scales[i] = mix(1.0, mix(scale_range.x, scale_range.y, rand[i].y), scale);
106+
offsets[i] = vector2(
107+
mix(offset_range.x, offset_range.y, rand[i].x * offset),
108+
mix(offset_range.x, offset_range.y, rand[i].y * offset));
109+
}
110+
111+
vector sin_r = sin(rotations);
112+
vector cos_r = cos(rotations);
113+
114+
HextileData tile_data;
115+
tile_data.weights = vector(w1, w2, w3);
116+
tile_data.rotations = rotations;
117+
118+
// Transform coordinates for each tile
119+
for (int i = 0; i < 3; i++)
120+
{
121+
vector2 d = coord - ctr[i];
122+
float c = cos_r[i];
123+
float s = sin_r[i];
124+
float sc = scales[i];
125+
126+
tile_data.coords[i] = vector2(
127+
(d.x * c - d.y * s) / sc + ctr[i].x + offsets[i].x,
128+
(d.x * s + d.y * c) / sc + ctr[i].y + offsets[i].y);
129+
}
130+
131+
return tile_data;
132+
}
133+
134+
// Blend 3 normals by blending the gradients
135+
// Morten S. Mikkelsen, Surface Gradient-Based Bump Mapping Framework, Journal of
136+
// Computer Graphics Techniques (JCGT), vol. 9, no. 3, 60-90, 2020
137+
// http://jcgt.org/published/0009/03/04/
138+
vector mx_normals_to_gradient(vector N, vector Np)
139+
{
140+
float d = dot(N, Np);
141+
vector g = (d * N - Np) / max(1e-8, abs(d));
142+
return g;
143+
}
144+
145+
vector mx_gradient_blend_3_normals(vector N, vector N1, float N1_weight, vector N2, float N2_weight, vector N3, float N3_weight)
146+
{
147+
float w1 = clamp(N1_weight, 0.0, 1.0);
148+
float w2 = clamp(N2_weight, 0.0, 1.0);
149+
float w3 = clamp(N3_weight, 0.0, 1.0);
150+
151+
vector g1 = mx_normals_to_gradient(N, N1);
152+
vector g2 = mx_normals_to_gradient(N, N2);
153+
vector g3 = mx_normals_to_gradient(N, N3);
154+
155+
// Blend
156+
vector gg = w1 * g1 + w2 * g2 + w3 * g3;
157+
158+
// Gradient to normal
159+
return normalize(N - gg);
160+
}
161+
162+
// Axis-angle rotation matrix
163+
matrix mx_axis_rotation_matrix(vector axis, float angle)
164+
{
165+
float s = sin(angle);
166+
float c = cos(angle);
167+
float omc = 1.0 - c;
168+
vector a = axis;
169+
170+
return matrix(
171+
a[0]*a[0]*omc + c, a[0]*a[1]*omc - a[2]*s, a[0]*a[2]*omc + a[1]*s, 0.0,
172+
a[1]*a[0]*omc + a[2]*s, a[1]*a[1]*omc + c, a[1]*a[2]*omc - a[0]*s, 0.0,
173+
a[2]*a[0]*omc - a[1]*s, a[2]*a[1]*omc + a[0]*s, a[2]*a[2]*omc + c, 0.0,
174+
0.0, 0.0, 0.0, 1.0
175+
);
176+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#include "lib/$fileTransformUv"
2+
#include "lib/mx_hextile.osl"
3+
4+
// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
5+
// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
6+
// http://jcgt.org/published/0011/03/05/
7+
void mx_hextiledimage_color3(
8+
textureresource file,
9+
color default_value,
10+
vector2 texcoord,
11+
vector2 tiling,
12+
float rotation,
13+
vector2 rotationrange,
14+
float scale,
15+
vector2 scalerange,
16+
float offset,
17+
vector2 offsetrange,
18+
float falloff,
19+
float falloffcontrast,
20+
color lumacoeffs,
21+
output color result)
22+
{
23+
if (file.filename == "")
24+
{
25+
result = default_value;
26+
return;
27+
}
28+
29+
vector2 coord = mx_transform_uv(texcoord) * tiling;
30+
31+
HextileData tile_data = mx_hextile_coord(coord, rotation, rotationrange, scale, scalerange, offset, offsetrange);
32+
33+
// Sample textures for each tile
34+
color c[3];
35+
vector cw;
36+
37+
for (int i = 0; i < 3; i++)
38+
{
39+
c[i] = texture(file.filename, tile_data.coords[i].x, tile_data.coords[i].y,
40+
"missingcolor", default_value,
41+
"swrap", "periodic", "twrap", "periodic"
42+
#if (OSL_VERSION_MAJOR == 1 && OSL_VERSION_MINOR >= 14) || (OSL_VERSION_MAJOR > 1)
43+
, "colorspace", file.colorspace
44+
#endif
45+
);
46+
cw[i] = dot(vector(c[i]), vector(lumacoeffs));
47+
}
48+
49+
// Luminance-based blend weights
50+
cw = mix(vector(1.0), cw, vector(falloffcontrast));
51+
vector w = mx_hextile_compute_blend_weights(cw, tile_data.weights, falloff);
52+
53+
// Blend colors
54+
result = w[0] * c[0] + w[1] * c[1] + w[2] * c[2];
55+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include "lib/$fileTransformUv"
2+
#include "lib/mx_hextile.osl"
3+
4+
// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
5+
// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
6+
// http://jcgt.org/published/0011/03/05/
7+
void mx_hextiledimage_color4(
8+
textureresource file,
9+
color4 default_value,
10+
vector2 texcoord,
11+
vector2 tiling,
12+
float rotation,
13+
vector2 rotationrange,
14+
float scale,
15+
vector2 scalerange,
16+
float offset,
17+
vector2 offsetrange,
18+
float falloff,
19+
float falloffcontrast,
20+
color lumacoeffs,
21+
output color4 result)
22+
{
23+
if (file.filename == "")
24+
{
25+
result = default_value;
26+
return;
27+
}
28+
29+
vector2 coord = mx_transform_uv(texcoord) * tiling;
30+
31+
HextileData tile_data = mx_hextile_coord(coord, rotation, rotationrange, scale, scalerange, offset, offsetrange);
32+
33+
// Sample textures for each tile
34+
color c[3];
35+
float alpha[3];
36+
vector cw;
37+
38+
for (int i = 0; i < 3; i++)
39+
{
40+
c[i] = texture(file.filename, tile_data.coords[i].x, tile_data.coords[i].y,
41+
"alpha", alpha[i],
42+
"missingcolor", default_value.rgb, "missingalpha", default_value.a,
43+
"swrap", "periodic", "twrap", "periodic"
44+
#if (OSL_VERSION_MAJOR == 1 && OSL_VERSION_MINOR >= 14) || (OSL_VERSION_MAJOR > 1)
45+
, "colorspace", file.colorspace
46+
#endif
47+
);
48+
cw[i] = dot(vector(c[i]), vector(lumacoeffs));
49+
}
50+
51+
// Luminance-based blend weights
52+
cw = mix(vector(1.0), cw, vector(falloffcontrast));
53+
vector w = mx_hextile_compute_blend_weights(cw, tile_data.weights, falloff);
54+
55+
// Alpha (average with optional falloff adjustment)
56+
float a = (alpha[0] + alpha[1] + alpha[2]) / 3.0;
57+
if (falloff != 0.5)
58+
{
59+
a = mx_schlick_gain(a, falloff);
60+
}
61+
62+
// Blend colors
63+
result.rgb = w[0] * c[0] + w[1] * c[1] + w[2] * c[2];
64+
result.a = a;
65+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include "lib/$fileTransformUv"
2+
#include "lib/mx_hextile.osl"
3+
4+
// Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, Journal of Computer Graphics
5+
// Techniques (JCGT), vol. 11, no. 2, 77-94, 2022
6+
// http://jcgt.org/published/0011/03/05/
7+
void mx_hextilednormalmap_vector3(
8+
textureresource file,
9+
vector default_value,
10+
vector2 texcoord,
11+
vector2 tiling,
12+
float rotation,
13+
vector2 rotationrange,
14+
float scale,
15+
vector2 scalerange,
16+
float offset,
17+
vector2 offsetrange,
18+
float falloff,
19+
float strength,
20+
int flip_g,
21+
vector N,
22+
vector T,
23+
vector B,
24+
output vector result)
25+
{
26+
if (file.filename == "")
27+
{
28+
result = N;
29+
return;
30+
}
31+
32+
vector2 coord = mx_transform_uv(texcoord) * tiling;
33+
34+
HextileData tile_data = mx_hextile_coord(coord, rotation, rotationrange, scale, scalerange, offset, offsetrange);
35+
36+
// Process each tile: sample, decode, transform, and compute normals
37+
vector tile_normals[3];
38+
39+
for (int i = 0; i < 3; i++)
40+
{
41+
// Sample normal map for this tile
42+
color nm_raw = texture(file.filename, tile_data.coords[i].x, tile_data.coords[i].y,
43+
"missingcolor", color(default_value),
44+
"swrap", "periodic", "twrap", "periodic");
45+
46+
// Convert to vector and decode normal map
47+
vector nm = vector(nm_raw);
48+
if (flip_g)
49+
nm[1] = 1.0 - nm[1];
50+
nm = 2.0 * nm - 1.0;
51+
52+
// Rotate tangent frame for this tile
53+
matrix tangent_rot_mat = mx_axis_rotation_matrix(N, -tile_data.rotations[i]);
54+
vector Ti = transform(tangent_rot_mat, T) * strength;
55+
vector Bi = transform(tangent_rot_mat, B) * strength;
56+
57+
// The OSL backend uses dPdu and dPdv for tangents and bitangents, but these vectors are not
58+
// guaranteed to be orthonormal. Orthogonalize the tangent frame using Gram-Schmidt.
59+
vector Tn = normalize(Ti - dot(Ti, N) * N);
60+
vector Bn = normalize(Bi - dot(Bi, N) * N - dot(Bi, Tn) * Tn);
61+
62+
tile_normals[i] = normalize(Tn * nm[0] + Bn * nm[1] + N * nm[2]);
63+
}
64+
65+
// Blend weights and normals using gradient blending
66+
vector w = mx_hextile_compute_blend_weights(vector(1.0), tile_data.weights, falloff);
67+
result = mx_gradient_blend_3_normals(N, tile_normals[0], w[0], tile_normals[1], w[1], tile_normals[2], w[2]);
68+
}

libraries/stdlib/genosl/stdlib_genosl_impl.mtlx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,25 @@
6767
<input name="default" type="vector4" implname="default_value" />
6868
</implementation>
6969

70+
<!-- <hextiledimage> -->
71+
<implementation name="IM_hextiledimage_color3_genosl" nodedef="ND_hextiledimage_color3" file="mx_hextiledimage_color3.osl" function="mx_hextiledimage_color3" target="genosl">
72+
<input name="default" type="color3" implname="default_value" />
73+
</implementation>
74+
<implementation name="IM_hextiledimage_color4_genosl" nodedef="ND_hextiledimage_color4" file="mx_hextiledimage_color4.osl" function="mx_hextiledimage_color4" target="genosl">
75+
<input name="default" type="color4" implname="default_value" />
76+
</implementation>
77+
7078
<!-- <triplanarprojection> -->
7179

7280
<!-- <normalmap> -->
7381
<implementation name="IM_normalmap_float_genosl" nodedef="ND_normalmap_float" file="mx_normalmap.osl" function="mx_normalmap_float" target="genosl" />
7482
<implementation name="IM_normalmap_vector2_genosl" nodedef="ND_normalmap_vector2" file="mx_normalmap.osl" function="mx_normalmap_vector2" target="genosl" />
7583

84+
<!-- <hextilednormalmap> -->
85+
<implementation name="IM_hextilednormalmap_vector3_genosl" nodedef="ND_hextilednormalmap_vector3" file="mx_hextilednormalmap_vector3.osl" function="mx_hextilednormalmap_vector3" target="genosl">
86+
<input name="default" type="vector3" implname="default_value" />
87+
</implementation>
88+
7689
<!-- ======================================================================== -->
7790
<!-- Procedural nodes -->
7891
<!-- ======================================================================== -->

0 commit comments

Comments
 (0)