From 3feb06385eb2d500bd0827e60302a196784d2edf Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:56:29 +0000 Subject: [PATCH] Hex Tiling --- schemas/materials.schema.json | 17 ++ .../java/rs117/hd/config/HexTilingMode.java | 7 + .../hd/opengl/uniforms/UBOMaterials.java | 2 + .../rs117/hd/scene/materials/Material.java | 7 + .../resources/rs117/hd/scene/materials.json | 10 +- src/main/resources/rs117/hd/scene_frag.glsl | 31 ++- .../rs117/hd/uniforms/materials.glsl | 8 +- .../resources/rs117/hd/utils/hex_tiling.glsl | 180 ++++++++++++++++++ src/main/resources/rs117/hd/utils/misc.glsl | 27 +++ 9 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 src/main/java/rs117/hd/config/HexTilingMode.java create mode 100644 src/main/resources/rs117/hd/utils/hex_tiling.glsl diff --git a/schemas/materials.schema.json b/schemas/materials.schema.json index 4eee8abaf4..73870a7339 100644 --- a/schemas/materials.schema.json +++ b/schemas/materials.schema.json @@ -87,6 +87,23 @@ "type": "number", "description": "The gloss of specular reflections. Defaults to 0. Commonly ranges from 10 to 500." }, + "hexTilingScale": { + "type": "number", + "description": "The scale of the hexagonal tiling pattern. Defaults to 0.0, i.e. no tiling." + }, + "hexTilingBlend": { + "type": "number", + "description": "The blending of the hexagonal tiling pattern. Defaults to 4.0." + }, + "hexTilingMode": { + "type": "string", + "description": "", + "enum": [ + "OFFSET", + "OFFSET_WITH_MIRROR", + "OFFSET_WITH_ROTATION" + ] + }, "scrollSpeed": { "type": "array", "description": "The duration in seconds, per UV direction, before texture scrolling repeats. Defaults to [ 0, 0 ], which disables scrolling.", diff --git a/src/main/java/rs117/hd/config/HexTilingMode.java b/src/main/java/rs117/hd/config/HexTilingMode.java new file mode 100644 index 0000000000..cd0e3bfec0 --- /dev/null +++ b/src/main/java/rs117/hd/config/HexTilingMode.java @@ -0,0 +1,7 @@ +package rs117.hd.config; + +public enum HexTilingMode { + OFFSET, + OFFSET_WITH_MIRROR, + OFFSET_WITH_ROTATION +} diff --git a/src/main/java/rs117/hd/opengl/uniforms/UBOMaterials.java b/src/main/java/rs117/hd/opengl/uniforms/UBOMaterials.java index 3e2e3bb0d0..fa0c3b5ce7 100644 --- a/src/main/java/rs117/hd/opengl/uniforms/UBOMaterials.java +++ b/src/main/java/rs117/hd/opengl/uniforms/UBOMaterials.java @@ -24,6 +24,8 @@ public static class MaterialStruct extends StructProperty { public Property displacementScale = addProperty(PropertyType.Float, "displacementScale"); public Property specularStrength = addProperty(PropertyType.Float, "specularStrength"); public Property specularGloss = addProperty(PropertyType.Float, "specularGloss"); + public Property hexTilingScale = addProperty(PropertyType.Float, "hexTilingScale"); + public Property hexTilingBlend = addProperty(PropertyType.Float, "hexTilingBlend"); public Property flowMapStrength = addProperty(PropertyType.Float, "flowMapStrength"); public Property flowMapDuration = addProperty(PropertyType.FVec2, "flowMapDuration"); public Property scrollDuration = addProperty(PropertyType.FVec2, "scrollDuration"); diff --git a/src/main/java/rs117/hd/scene/materials/Material.java b/src/main/java/rs117/hd/scene/materials/Material.java index 1eaf988cff..7053173065 100644 --- a/src/main/java/rs117/hd/scene/materials/Material.java +++ b/src/main/java/rs117/hd/scene/materials/Material.java @@ -18,6 +18,7 @@ import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; +import rs117.hd.config.HexTilingMode; import rs117.hd.opengl.uniforms.UBOMaterials; import rs117.hd.scene.MaterialManager; import rs117.hd.scene.model_overrides.ModelOverride; @@ -60,6 +61,9 @@ public class Material { @JsonAdapter(ColorUtils.LinearAdapter.class) public float brightness = 1; private float displacementScale = .1f; + private float hexTilingScale = 0.0f; + private float hexTilingBlend = 4.0f; + private HexTilingMode hexTilingMode = HexTilingMode.OFFSET_WITH_ROTATION; private float flowMapStrength; private float[] flowMapDuration = { 0, 0 }; private float specularStrength; @@ -209,6 +213,7 @@ public void fillMaterialStruct( struct.flowMap.set(getTextureLayer(flowMap)); struct.shadowAlphaMap.set(getTextureLayer(shadowAlphaMap)); struct.flags.set( + (hexTilingMode.ordinal() & 0x3) << 3 | (overrideBaseColor ? 1 : 0) << 2 | (unlit ? 1 : 0) << 1 | (hasTransparency ? 1 : 0) @@ -217,6 +222,8 @@ public void fillMaterialStruct( struct.displacementScale.set(displacementScale); struct.specularStrength.set(specularStrength); struct.specularGloss.set(specularGloss); + struct.hexTilingScale.set(hexTilingScale); + struct.hexTilingBlend.set(hexTilingBlend); struct.flowMapStrength.set(flowMapStrength); struct.flowMapDuration.set(flowMapDuration); struct.scrollDuration.set(scrollSpeedX, scrollSpeedY); diff --git a/src/main/resources/rs117/hd/scene/materials.json b/src/main/resources/rs117/hd/scene/materials.json index 6647af0c7d..65774b17c7 100644 --- a/src/main/resources/rs117/hd/scene/materials.json +++ b/src/main/resources/rs117/hd/scene/materials.json @@ -1325,7 +1325,9 @@ "name": "GRAVEL", "normalMap": "GRAVEL_N", "specularStrength": 0.4, - "specularGloss": 130.0 + "specularGloss": 130.0, + "hexTilingScale": 1.25, + "hexTilingBlend": 2.5 }, { "name": "VERTICAL_GRAVEL", @@ -1425,7 +1427,8 @@ "name": "GRUNGE_1", "normalMap": "GRUNGE_1_N", "specularStrength": 0.25, - "specularGloss": 30.0 + "specularGloss": 30.0, + "hexTilingScale": 4.25 }, { "name": "GRUNGE_1_LIGHT", @@ -3026,6 +3029,9 @@ "normalMap": "HD_HAY_N", "specularStrength": 0.175, "specularGloss": 15.0, + "hexTilingScale": 1.5, + "hexTilingBlend": 1.5, + "hexTilingMode": "OFFSET_WITH_MIRROR", "materialsToReplace": [ "HAY" ], diff --git a/src/main/resources/rs117/hd/scene_frag.glsl b/src/main/resources/rs117/hd/scene_frag.glsl index bcc9ac06f0..0e872cc630 100644 --- a/src/main/resources/rs117/hd/scene_frag.glsl +++ b/src/main/resources/rs117/hd/scene_frag.glsl @@ -31,6 +31,7 @@ #define DISPLAY_TANGENT 0 #define DISPLAY_SHADOWS 0 #define DISPLAY_LIGHTING 0 +#define DISPLAY_HEX 0 #include #include @@ -81,6 +82,7 @@ vec2 worldUvs(float scale) { #include #include #include +#include void main() { vec3 downDir = vec3(0, -1, 0); @@ -158,7 +160,6 @@ void main() { uv3 += uvFlow * flowMapStrength; // Set up tangent-space transformation matrix - vec3 N; #if FLAT_SHADING && ZONE_RENDERER N = normalize(fFlatNormal); @@ -205,6 +206,16 @@ void main() { fragPos += TBN * fragDelta; #endif + // Build HexData, if Terrain use world space XZ otherwise UVs + HexData hex1 = buildHexData(uv1, IN.position / TILE_SIZE, material1.hexTilingScale, material1.hexTilingBlend, getMaterialHexTilingMode(material1)); + HexData hex2 = buildHexData(uv2, IN.position / TILE_SIZE, material2.hexTilingScale, material2.hexTilingBlend, getMaterialHexTilingMode(material2)); + HexData hex3 = buildHexData(uv3, IN.position / TILE_SIZE, material3.hexTilingScale, material3.hexTilingBlend, getMaterialHexTilingMode(material3)); + + #if DISPLAY_HEX + FragColor = vec4(debugHex(hex1) * IN.texBlend.x + debugHex(hex2) * IN.texBlend.y + debugHex(hex3) * IN.texBlend.z, 1.0); + if (DISPLAY_HEX == 1) return; // Redundant, for syntax highlighting in IntelliJ + #endif + vec3 hsl1 = unpackRawHsl(fAlphaBiasHsl[0]); vec3 hsl2 = unpackRawHsl(fAlphaBiasHsl[1]); vec3 hsl3 = unpackRawHsl(fAlphaBiasHsl[2]); @@ -237,9 +248,9 @@ void main() { #endif // get diffuse textures - vec4 texColor1 = colorMap1 == -1 ? vec4(1) : texture(textureArray, vec3(uv1, colorMap1), mipBias); - vec4 texColor2 = colorMap2 == -1 ? vec4(1) : texture(textureArray, vec3(uv2, colorMap2), mipBias); - vec4 texColor3 = colorMap3 == -1 ? vec4(1) : texture(textureArray, vec3(uv3, colorMap3), mipBias); + vec4 texColor1 = colorMap1 == -1 ? vec4(1) : sampleHex(textureArray, vec3(uv1, colorMap1), hex1); + vec4 texColor2 = colorMap2 == -1 ? vec4(1) : sampleHex(textureArray, vec3(uv2, colorMap2), hex2); + vec4 texColor3 = colorMap3 == -1 ? vec4(1) : sampleHex(textureArray, vec3(uv3, colorMap3), hex3); texColor1.rgb *= material1.brightness; texColor2.rgb *= material2.brightness; texColor3.rgb *= material3.brightness; @@ -347,9 +358,9 @@ void main() { vec3 vSpecularGloss = vec3(material1.specularGloss, material2.specularGloss, material3.specularGloss); vec3 vSpecularStrength = vec3(material1.specularStrength, material2.specularStrength, material3.specularStrength); vSpecularStrength *= vec3( - material1.roughnessMap == -1 ? 1 : linearToSrgb(texture(textureArray, vec3(uv1, material1.roughnessMap)).r), - material2.roughnessMap == -1 ? 1 : linearToSrgb(texture(textureArray, vec3(uv2, material2.roughnessMap)).r), - material3.roughnessMap == -1 ? 1 : linearToSrgb(texture(textureArray, vec3(uv3, material3.roughnessMap)).r) + material1.roughnessMap == -1 ? 1 : linearToSrgb(sampleHex(textureArray, vec3(uv1, material1.roughnessMap), hex1).r), + material2.roughnessMap == -1 ? 1 : linearToSrgb(sampleHex(textureArray, vec3(uv2, material2.roughnessMap), hex2).r), + material3.roughnessMap == -1 ? 1 : linearToSrgb(sampleHex(textureArray, vec3(uv3, material3.roughnessMap), hex3).r) ); // apply specular highlights to anything semi-transparent @@ -372,9 +383,9 @@ void main() { vec3 ambientLightOut = ambientColor * ambientStrength; float aoFactor = - IN.texBlend.x * (material1.ambientOcclusionMap == -1 ? 1 : texture(textureArray, vec3(uv1, material1.ambientOcclusionMap)).r) + - IN.texBlend.y * (material2.ambientOcclusionMap == -1 ? 1 : texture(textureArray, vec3(uv2, material2.ambientOcclusionMap)).r) + - IN.texBlend.z * (material3.ambientOcclusionMap == -1 ? 1 : texture(textureArray, vec3(uv3, material3.ambientOcclusionMap)).r); + IN.texBlend.x * (material1.ambientOcclusionMap == -1 ? 1 : sampleHex(textureArray, vec3(uv1, material1.ambientOcclusionMap), hex1).r) + + IN.texBlend.y * (material2.ambientOcclusionMap == -1 ? 1 : sampleHex(textureArray, vec3(uv2, material2.ambientOcclusionMap), hex2).r) + + IN.texBlend.z * (material3.ambientOcclusionMap == -1 ? 1 : sampleHex(textureArray, vec3(uv3, material3.ambientOcclusionMap), hex3).r); ambientLightOut *= aoFactor; // directional light diff --git a/src/main/resources/rs117/hd/uniforms/materials.glsl b/src/main/resources/rs117/hd/uniforms/materials.glsl index 6f60529dbb..38ca96f326 100644 --- a/src/main/resources/rs117/hd/uniforms/materials.glsl +++ b/src/main/resources/rs117/hd/uniforms/materials.glsl @@ -10,11 +10,13 @@ struct Material { int ambientOcclusionMap; int flowMap; int shadowAlphaMap; - int flags; // overrideBaseColor << 2 | unlit << 1 | hasTransparency + int flags; // hexTilingMode (3 bits) << 3 | overrideBaseColor << 2 | unlit << 1 | hasTransparency float brightness; float displacementScale; float specularStrength; float specularGloss; + float hexTilingScale; + float hexTilingBlend; float flowMapStrength; vec2 flowMapDuration; vec2 scrollDuration; @@ -38,3 +40,7 @@ int getMaterialIsUnlit(const Material material) { bool getMaterialHasTransparency(const Material material) { return (material.flags & 1) == 1; } + +int getMaterialHexTilingMode(const Material material) { + return (material.flags >> 3) & 0x3; +} \ No newline at end of file diff --git a/src/main/resources/rs117/hd/utils/hex_tiling.glsl b/src/main/resources/rs117/hd/utils/hex_tiling.glsl new file mode 100644 index 0000000000..88adce09e1 --- /dev/null +++ b/src/main/resources/rs117/hd/utils/hex_tiling.glsl @@ -0,0 +1,180 @@ +#pragma once + +#include + +#define HEX_UV_OFFSET_ONLY 0 +#define HEX_UV_OFFSET_WITH_MIRROR 1 +#define HEX_UV_OFFSET_WITH_ROTATE 2 +#define HEX_UV_MODE HEX_UV_OFFSET_WITH_ROTATE + +#define HEX_EPS 0.001 +#define HEX_DOMINANT 0.95 + +struct HexData { + vec3 weights; // Barycentric weights for triangle interpolation + vec2 uv[3]; // Perturbed UV coordinates for each vertex + vec2 vertex[3]; // Hexagonal cell vertex positions + bool enabled; // Flag indicating if hex computation is valid + vec2 dPdx; // Partial derivative for gradient-aware sampling + vec2 dPdy; // Partial derivative for gradient-aware sampling + int dominantIdx; // Precomputed dominant vertex index +}; + +// Converts regular UV space into skewed hex space +const mat2 HEX_MATRIX = mat2( + 1.7320508, -1.0, + 0.0, 2.0 +); + +// Perturb UV coordinates for a hex vertex to create variation +vec2 makeUV(vec2 uv, vec2 vertexPos, int mode) { + vec4 h = hash24(vertexPos); + vec2 p = uv; + + if(mode == HEX_UV_OFFSET_WITH_ROTATE) { + p -= 0.5; + + // Discreate Rotation + float angle = h.x * 6.28318530718; + float s = sin(angle); + float c = cos(angle); + p = vec2( + c * p.x - s * p.y, + s * p.x + c * p.y + ); + + p += 0.5; + } else if(mode == HEX_UV_OFFSET_WITH_MIRROR) { + // Flip along U/W + vec2 flipMask = step(0.5, h.xy) * 2.0 - 1.0; + p *= flipMask; + } + + // Offset & Scale + float scaleJitter = mix(0.85, 1.15, h.z); + return p * scaleJitter + h.w; +} + +HexData buildHexData(vec2 uv, vec3 fragPos, float scale, float blend, int mode) { + HexData h; + if (scale <= 0.0) { + h.enabled = false; + return h; + } + h.enabled = true; + + // Derivatives (shared across samples) + h.dPdx = dFdx(fragPos.xz); + h.dPdy = dFdy(fragPos.xz); + + vec2 skew = fragPos.xz * scale * HEX_MATRIX; + vec2 base = floor(skew); + vec2 f = fract(skew); + + vec3 temp = vec3(f, 0.0); + temp.z = 1.0 - temp.x - temp.y; + + float s = step(0.0, -temp.z); + float s2 = 2.0 * s - 1.0; + temp *= s2; + + // Triangle vertices + vec2 v1 = base + vec2(s, s); + vec2 v2 = base + vec2(s, 1.0 - s); + vec2 v3 = base + vec2(1.0 - s, s); + + h.vertex[0] = v1; + h.vertex[1] = v2; + h.vertex[2] = v3; + + // Barycentric weights + vec3 w = vec3(-temp.z, s - temp.y, s - temp.x); + w = max(w, 0.0); + + // Sharpen blend + w = pow(w, vec3(7.0 * blend)); + + // Normalize + float invSum = 1.0 / (w.x + w.y + w.z); + h.weights = w * invSum; + + // UVs + h.uv[0] = makeUV(uv, v1, mode); + h.uv[1] = makeUV(uv, v2, mode); + h.uv[2] = makeUV(uv, v3, mode); + + float maxW = max(max(h.weights.x, h.weights.y), h.weights.z); + if (maxW > HEX_DOMINANT) { + // Determine which weight is largest + h.dominantIdx = (h.weights.x > h.weights.y) + ? (h.weights.x > h.weights.z ? 0 : 2) + : (h.weights.y > h.weights.z ? 1 : 2); + } else { + h.dominantIdx = -1; // no dominant vertex + } + + return h; +} + +vec4 sampleHex(sampler2D tex, HexData h) { + if (!h.enabled) + return texture(tex, h.uv[0]); + + if (h.dominantIdx >= 0) + return textureGrad(tex, h.uv[h.dominantIdx], h.dPdx, h.dPdy); + + vec4 c0 = textureGrad(tex, h.uv[0], h.dPdx, h.dPdy); + vec4 c1 = textureGrad(tex, h.uv[1], h.dPdx, h.dPdy); + vec4 c2 = textureGrad(tex, h.uv[2], h.dPdx, h.dPdy); + + return c0 * h.weights.x + c1 * h.weights.y + c2 * h.weights.z; +} + +vec3 sampleHexRGB(sampler2D tex, HexData h) { + return sampleHex(tex, h).rgb; +} + +vec4 sampleHex(sampler2DArray tex, vec3 uvw, HexData h) { + if (!h.enabled) + return texture(tex, uvw); + + if (h.dominantIdx >= 0) + return textureGrad(tex, vec3(h.uv[h.dominantIdx], uvw.z), h.dPdx, h.dPdy); + + vec4 c0 = textureGrad(tex, vec3(h.uv[0], uvw.z), h.dPdx, h.dPdy); + vec4 c1 = textureGrad(tex, vec3(h.uv[1], uvw.z), h.dPdx, h.dPdy); + vec4 c2 = textureGrad(tex, vec3(h.uv[2], uvw.z), h.dPdx, h.dPdy); + + return c0 * h.weights.x + c1 * h.weights.y + c2 * h.weights.z; +} + +vec3 sampleHexRGB(sampler2DArray tex, vec3 uvw, HexData h) { + return sampleHex(tex, uvw, h).rgb; +} + +vec3 debugHex(HexData h) { + if (!h.enabled) return vec3(0.0); + + vec3 W = h.weights; + + vec2 v1 = h.vertex[0]; + vec2 v2 = h.vertex[1]; + vec2 v3 = h.vertex[2]; + + vec3 res = vec3(0.0); + + int i1 = int(v1.x - v1.y) % 3; + if (i1 < 0) i1 += 3; + + int hi = (i1 < 2) ? (i1 + 1) : 0; + int lo = (i1 > 0) ? (i1 - 1) : 2; + + int i2 = (v1.x < v3.x) ? lo : hi; + int i3 = (v1.x < v3.x) ? hi : lo; + + res.x = (i3 == 0) ? W.z : ((i2 == 0) ? W.y : W.x); + res.y = (i3 == 1) ? W.z : ((i2 == 1) ? W.y : W.x); + res.z = (i3 == 2) ? W.z : ((i2 == 2) ? W.y : W.x); + + return res; +} \ No newline at end of file diff --git a/src/main/resources/rs117/hd/utils/misc.glsl b/src/main/resources/rs117/hd/utils/misc.glsl index 6e5406693b..8853048859 100644 --- a/src/main/resources/rs117/hd/utils/misc.glsl +++ b/src/main/resources/rs117/hd/utils/misc.glsl @@ -112,6 +112,33 @@ void undoVanillaShading(inout int hsl, vec3 unrotatedNormal) { } #endif +vec2 rotate(vec2 p, float angle) { + float s = sin(angle); + float c = cos(angle); + return vec2( + c * p.x - s * p.y, + s * p.x + c * p.y + ); +} + +vec4 hash24(vec2 p) { + vec4 p4 = fract(vec4(p.xyxy) * vec4(0.1031, 0.1030, 0.0973, 0.1099)); + p4 += dot(p4, p4.wzxy + 33.33); + return fract((p4.xxyz + p4.yzzw) * p4.zywx); +} + +vec3 hash23(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(vec3(p.x + p.y, p.x * 1.345, p.y * 2.123)); +} + +vec2 hash22(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 78.233); + return fract(vec2(p.x * p.y, p.x + p.y)); +} + // 2D Random float hash(in vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);