Skip to content

Instantly share code, notes, and snippets.

@Ctrlmonster
Created May 14, 2025 07:18
Show Gist options
  • Save Ctrlmonster/cc4a47c7483db0e3baf790ea7d250536 to your computer and use it in GitHub Desktop.
Save Ctrlmonster/cc4a47c7483db0e3baf790ea7d250536 to your computer and use it in GitHub Desktop.
Lod dither fade for mesh and batched mesh
// for regular meshes
import {ShaderMaterial, Vector2, Color, Texture} from "three";
export class LodFadeMaterial extends ShaderMaterial {
constructor(map: Texture, ditherOffset: number, ditherThreshold: number, invertMask: boolean, resolution: Vector2, color: Color) {
super({
uniforms: {
map: {value: map},
resolution: {value: resolution},
ditherOffset: {value: ditherOffset},
ditherThreshold: {value: ditherThreshold},
invertMask: {value: invertMask},
color: {value: color},
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform vec2 resolution;
uniform float ditherOffset;
uniform float ditherThreshold;
uniform vec3 color; // Debug color
uniform bool invertMask;
uniform sampler2D map;
const mat4 bayerMatrix = mat4(
0.0, 0.5, 0.125, 0.625,
0.75, 0.25, 0.875, 0.375,
0.1875, 0.6875, 0.0625, 0.5625,
0.9375, 0.4375, 0.8125, 0.3125
);
float dither(vec2 uv, float offset) {
vec2 gridPos = mod(floor(uv * 4.0), 4.0);
float value = bayerMatrix[int(gridPos.y)][int(gridPos.x)];
return mod(value + offset, 1.0);
}
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
float threshold = dither(uv, ditherOffset);
bool shouldDiscard = invertMask ? (threshold >= ditherThreshold) : (threshold < ditherThreshold);
if (shouldDiscard) discard;
// Use the uniform color for debugging
vec3 color0 = texture2D(map, vUv).rgb;
gl_FragColor = vec4(color0 * color, 1.0);
}
`,
});
}
/**
* Updates the resolution uniform.
* @param resolution New resolution value.
*/
setResolution(resolution: number) {
this.uniforms.resolution.value.set(resolution, resolution);
}
setDitherThreshold(threshold: number) {
this.uniforms.ditherThreshold.value = threshold;
}
setInvertMask(invertMask: boolean) {
this.uniforms.invertMask.value = invertMask;
}
}
// =============================================================================================================
// for batched mesh using UBOs
import {Color, ShaderMaterial, StaticDrawUsage, Texture, Uniform, UniformsGroup, Vector2, Vector4} from "three";
const INIT_DITHER_OFFSET = 0.0;
const INIT_DITHER_THRESHOLD = 0.0;//0.5;
const INIT_INVERT_MASK_ = 0; // <- false
const INIT_DITHER_RESOLUTION = new Vector2(4, 4);
//const INIT_COLOR = new Color("white");
const INIT_DITHER_SETTINGS = {
ditherOffset: INIT_DITHER_OFFSET,
ditherThreshold: INIT_DITHER_THRESHOLD,
invertMask: INIT_INVERT_MASK_,
resolution: INIT_DITHER_RESOLUTION,
};
export class LodFadeBatchMaterial extends ShaderMaterial {
ditherUniformsGroup: UniformsGroup;
readonly isLodMaterial = true;
constructor(maxBatchInstances: number, map: Texture, color: Color, initDitherSettings = INIT_DITHER_SETTINGS) {
/*const color = new Color(Math.random(), Math.random(), Math.random());
console.log("Inside constructor of lod fade batch material:");
console.log("args: ", maxBatchInstances, map);*/
if (!maxBatchInstances || !map || !color) {
throw new Error(`args not there`);
}
super({
//wireframe: true,
uniforms: {
map: {value: map},
debugColor: {value: color}
},
defines: {
MAX_BATCH_INSTANCE_COUNT: maxBatchInstances
},
vertexShader: `
varying vec2 vUv;
flat out int vBatchId;
#if ! defined( GL_ANGLE_multi_draw )
#define gl_DrawID _gl_DrawID
uniform int _gl_DrawID;
#endif
uniform highp sampler2D batchingTexture;
uniform highp usampler2D batchingIdTexture;
mat4 getBatchingMatrix( const in float i ) {
int size = textureSize( batchingTexture, 0 ).x;
int j = int( i ) * 4;
int x = j % size;
int y = j / size;
vec4 v1 = texelFetch( batchingTexture, ivec2( x, y ), 0 );
vec4 v2 = texelFetch( batchingTexture, ivec2( x + 1, y ), 0 );
vec4 v3 = texelFetch( batchingTexture, ivec2( x + 2, y ), 0 );
vec4 v4 = texelFetch( batchingTexture, ivec2( x + 3, y ), 0 );
return mat4( v1, v2, v3, v4 );
}
float getIndirectIndex( const in int i ) {
int size = textureSize( batchingIdTexture, 0 ).x;
int x = i % size;
int y = i / size;
return float( texelFetch( batchingIdTexture, ivec2( x, y ), 0 ).r );
}
void main() {
vUv = uv;
float indirectIdx = getIndirectIndex( gl_DrawID );
mat4 batchingMatrix = getBatchingMatrix( indirectIdx );
// pass the batch id to the fragment shader
vBatchId = int(indirectIdx);
// multiply batch position with per-instance matrix
vec4 instancePosition = (batchingMatrix * vec4( position, 1.0 )).xyzw;
// Compute view-space position.
vec4 mvPosition = modelViewMatrix * instancePosition;
// Transform vertex to clip space.
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
#ifndef MAX_BATCH_INSTANCE_COUNT
#define MAX_BATCH_INSTANCE_COUNT 64
#endif
varying vec2 vUv;
flat in int vBatchId;
uniform sampler2D map;
uniform vec3 debugColor;
/*layout (std140) uniform DitherSettings {
vec4 packedSettings0[MAX_BATCH_INSTANCE_COUNT];
vec4 packedSettings1[MAX_BATCH_INSTANCE_COUNT];
};*/
/*
const mat4 bayerMatrix = mat4(
0.0, 0.5, 0.125, 0.625,
0.75, 0.25, 0.875, 0.375,
0.1875, 0.6875, 0.0625, 0.5625,
0.9375, 0.4375, 0.8125, 0.3125
);
float dither(vec2 uv, float offset) {
vec2 gridPos = mod(floor(uv * 4.0), 4.0);
float value = bayerMatrix[int(gridPos.y)][int(gridPos.x)];
return mod(value + offset, 1.0);
}*/
void main() {
/*
vec4 packed0 = packedSettings0[vBatchId].rgba;
float ditherOffset = packed0.x;
float ditherThreshold = packed0.y;
vec2 ditherResolution = vec2(4.0, 4.0);
//vec2 ditherResolution = vec2(packedSettings0[vBatchId].z, packedSettings0[vBatchId].w);
vec2 ditherUv = gl_FragCoord.xy / ditherResolution;
bool invertMask = packed0.z > 0.5;
float ditherValue = dither(ditherUv, ditherOffset);
bool shouldDiscard = invertMask ? (ditherValue >= ditherThreshold) : (ditherValue < ditherThreshold);
if (shouldDiscard) discard;*/
//vec3 color = packedSettings1[vBatchId].xyz;
// Use the uniform color for debugging
vec3 albedo = texture2D(map, vUv).rgb;
gl_FragColor = vec4(albedo, 1.0);
}
`,
});
// ------- Pack Settings into UniformsGroups -------------------------------
const N = maxBatchInstances;
this.ditherUniformsGroup = new UniformsGroup();
this.ditherUniformsGroup.setName("DitherSettings")
.setUsage( StaticDrawUsage );
const packedSettings0 = Array.from({length: N})
.map(() => {
return new Uniform(new Vector4(
initDitherSettings.ditherOffset,
initDitherSettings.ditherThreshold,
initDitherSettings.resolution.x,
initDitherSettings.resolution.y
));
});
const packedSettings1 = Array.from({length: N})
.map(() => {
return new Uniform(new Vector4(
color.r,
color.g,
color.b,
initDitherSettings.invertMask,
));
});
// add all uniforms groups
this.ditherUniformsGroup.add(packedSettings0);
this.ditherUniformsGroup.add(packedSettings1);
// -----------------------------------------------------------------------
/*this.uniformsGroups = [
this.ditherUniformsGroup
]*/
//console.log(packedSettings1, packedSettings0, maxBatchInstances);
}
clone(): this {
throw new Error(`LodFadeBatchMaterial can't be cloned – needs to be created manually`);
}
setResolution(batchInstanceId: number, value: Vector2) {
const uniform = (this.ditherUniformsGroup.uniforms[0] as Uniform[])[batchInstanceId].value;
uniform.z = value.x;
uniform.w = value.y;
}
setDitherThreshold(batchInstanceId: number, value: number) {
(this.ditherUniformsGroup.uniforms[0] as Uniform[])[batchInstanceId].value.y = value;
}
setDitherOffset(batchInstanceId: number, value: number) {
(this.ditherUniformsGroup.uniforms[0] as Uniform[])[batchInstanceId].value.x = value;
}
setInvertMask(batchInstanceId: number, value: boolean) {
(this.ditherUniformsGroup.uniforms[0] as Uniform[])[batchInstanceId].value.z = Number(value);
//(this.ditherUniformsGroup.uniforms[1] as Uniform[])[batchInstanceId].value.w = Number(value);
}
setColor(batchInstanceId: number, value: Color) {
const uniform = (this.ditherUniformsGroup.uniforms[1] as Uniform[])[batchInstanceId].value;
uniform.setX(value.r);
uniform.setY(value.g);
uniform.setZ(value.b);
return;
/*
//console.log(this.colorUniforms[ batchInstanceId ].value)
this.colorUniforms[ batchInstanceId ].value.setX(value.r);
this.colorUniforms[ batchInstanceId ].value.setY(value.g);
this.colorUniforms[ batchInstanceId ].value.setZ(value.g);
//(this.ditherUniformsGroup.uniforms[1] as Uniform[]).forEach(uniform => uniform.value = new Color("red"));
this.needsUpdate = true;
this.uniformsNeedUpdate = true;*/
//const uniform = (this.ditherUniformsGroup.uniforms[1] as Uniform[])[batchInstanceId].value;
// uniform.set(1.0, 0.0, 0.0) // testing, even this doesn't work!
/*uniform.x = value.r;
uniform.y = value.g;
uniform.z = value.b*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment