|
| 1 | +import { |
| 2 | + drawPass, ensureTexture, getGLCtx, getQuadVAO, glAvailable, |
| 3 | + linkProgram, readoutToCanvas, resizeGLCanvas, uploadSourceTexture, |
| 4 | + type Program, |
| 5 | +} from "gl"; |
| 6 | + |
| 7 | +// Perlin / Simplex / Worley FBM noise generator mixed over the source. |
| 8 | +// The hash uses exact uint32 wrap (vs JS float64 mid-multiply precision |
| 9 | +// loss), which shifts the exact pattern but keeps the noise character. |
| 10 | +const FS = `#version 300 es |
| 11 | +precision highp float; |
| 12 | +in vec2 v_uv; |
| 13 | +out vec4 fragColor; |
| 14 | +uniform sampler2D u_source; |
| 15 | +uniform vec2 u_res; |
| 16 | +uniform int u_type; // 0 gradient/perlin, 1 simplex, 2 worley |
| 17 | +uniform float u_scale; |
| 18 | +uniform int u_octaves; |
| 19 | +uniform int u_seed; |
| 20 | +uniform int u_frame; |
| 21 | +uniform int u_colorize; |
| 22 | +uniform float u_mix; |
| 23 | +
|
| 24 | +uint hashU(int x, int y, int seed) { |
| 25 | + uint h = uint(seed) + uint(x) * 374761393u + uint(y) * 668265263u; |
| 26 | + h = (h ^ (h >> 13u)) * 1274126177u; |
| 27 | + return h ^ (h >> 16u); |
| 28 | +} |
| 29 | +
|
| 30 | +float hashF(int x, int y, int seed) { |
| 31 | + return float(hashU(x, y, seed)) / 4294967296.0; |
| 32 | +} |
| 33 | +
|
| 34 | +float gradientNoise(float px, float py, int seed) { |
| 35 | + int x0 = int(floor(px)); |
| 36 | + int y0 = int(floor(py)); |
| 37 | + float fx = px - float(x0); |
| 38 | + float fy = py - float(y0); |
| 39 | + float u = fx * fx * (3.0 - 2.0 * fx); |
| 40 | + float v = fy * fy * (3.0 - 2.0 * fy); |
| 41 | +
|
| 42 | + float gxs[4] = float[4]( 1.0, -1.0, 1.0, -1.0); |
| 43 | + float gys[4] = float[4]( 1.0, 1.0, -1.0, -1.0); |
| 44 | +
|
| 45 | + int h00 = int(hashU(x0, y0, seed) & 3u); |
| 46 | + int h10 = int(hashU(x0 + 1, y0, seed) & 3u); |
| 47 | + int h01 = int(hashU(x0, y0 + 1, seed) & 3u); |
| 48 | + int h11 = int(hashU(x0 + 1, y0 + 1, seed) & 3u); |
| 49 | + float n00 = gxs[h00] * (px - float(x0)) + gys[h00] * (py - float(y0)); |
| 50 | + float n10 = gxs[h10] * (px - float(x0 + 1)) + gys[h10] * (py - float(y0)); |
| 51 | + float n01 = gxs[h01] * (px - float(x0)) + gys[h01] * (py - float(y0 + 1)); |
| 52 | + float n11 = gxs[h11] * (px - float(x0 + 1)) + gys[h11] * (py - float(y0 + 1)); |
| 53 | + float nx0 = n00 + u * (n10 - n00); |
| 54 | + float nx1 = n01 + u * (n11 - n01); |
| 55 | + return nx0 + v * (nx1 - nx0); |
| 56 | +} |
| 57 | +
|
| 58 | +float simplexNoise(float px, float py, int seed) { |
| 59 | + float F2 = 0.5 * (sqrt(3.0) - 1.0); |
| 60 | + float G2 = (3.0 - sqrt(3.0)) / 6.0; |
| 61 | + float s = (px + py) * F2; |
| 62 | + int i = int(floor(px + s)); |
| 63 | + int j = int(floor(py + s)); |
| 64 | + float t = float(i + j) * G2; |
| 65 | + float x0 = px - (float(i) - t); |
| 66 | + float y0 = py - (float(j) - t); |
| 67 | + int i1 = x0 > y0 ? 1 : 0; |
| 68 | + int j1 = x0 > y0 ? 0 : 1; |
| 69 | + float x1 = x0 - float(i1) + G2; |
| 70 | + float y1 = y0 - float(j1) + G2; |
| 71 | + float x2 = x0 - 1.0 + 2.0 * G2; |
| 72 | + float y2 = y0 - 1.0 + 2.0 * G2; |
| 73 | +
|
| 74 | + float gxs[4] = float[4]( 1.0, -1.0, 1.0, -1.0); |
| 75 | + float gys[4] = float[4]( 1.0, 1.0, -1.0, -1.0); |
| 76 | +
|
| 77 | + float result = 0.0; |
| 78 | + int ii[3]; int jj[3]; float xx[3]; float yy[3]; |
| 79 | + ii[0] = i; jj[0] = j; xx[0] = x0; yy[0] = y0; |
| 80 | + ii[1] = i + i1; jj[1] = j + j1; xx[1] = x1; yy[1] = y1; |
| 81 | + ii[2] = i + 1; jj[2] = j + 1; xx[2] = x2; yy[2] = y2; |
| 82 | + for (int k = 0; k < 3; k++) { |
| 83 | + float tval = 0.5 - xx[k] * xx[k] - yy[k] * yy[k]; |
| 84 | + if (tval < 0.0) continue; |
| 85 | + int h = int(hashU(ii[k], jj[k], seed) & 3u); |
| 86 | + result += tval * tval * tval * tval * (gxs[h] * xx[k] + gys[h] * yy[k]); |
| 87 | + } |
| 88 | + return 70.0 * result; |
| 89 | +} |
| 90 | +
|
| 91 | +float worleyNoise(float px, float py, int seed) { |
| 92 | + int ix = int(floor(px)); |
| 93 | + int iy = int(floor(py)); |
| 94 | + float minDist = 1e9; |
| 95 | + for (int dy = -1; dy <= 1; dy++) { |
| 96 | + for (int dx = -1; dx <= 1; dx++) { |
| 97 | + int cx = ix + dx; |
| 98 | + int cy = iy + dy; |
| 99 | + uint h = hashU(cx, cy, seed); |
| 100 | + float fpx = float(cx) + float(h & 0xffffu) / 65536.0; |
| 101 | + float fpy = float(cy) + float((h >> 16) & 0xffffu) / 65536.0; |
| 102 | + float ddx = px - fpx; |
| 103 | + float ddy = py - fpy; |
| 104 | + float d2 = ddx * ddx + ddy * ddy; |
| 105 | + if (d2 < minDist) minDist = d2; |
| 106 | + } |
| 107 | + } |
| 108 | + return sqrt(minDist); |
| 109 | +} |
| 110 | +
|
| 111 | +float sampleNoise(float px, float py, int seed) { |
| 112 | + if (u_type == 0) return gradientNoise(px, py, seed); |
| 113 | + if (u_type == 1) return simplexNoise(px, py, seed); |
| 114 | + return worleyNoise(px, py, seed); |
| 115 | +} |
| 116 | +
|
| 117 | +vec3 hslToRgb(float hue, float sat, float lit) { |
| 118 | + float c = (1.0 - abs(2.0 * lit - 1.0)) * sat; |
| 119 | + float hh = mod(mod(hue, 360.0) + 360.0, 360.0); |
| 120 | + float xc = c * (1.0 - abs(mod(hh / 60.0, 2.0) - 1.0)); |
| 121 | + float m = lit - c * 0.5; |
| 122 | + vec3 rgb; |
| 123 | + if (hh < 60.0) rgb = vec3(c, xc, 0.0); |
| 124 | + else if (hh < 120.0) rgb = vec3(xc, c, 0.0); |
| 125 | + else if (hh < 180.0) rgb = vec3(0.0, c, xc); |
| 126 | + else if (hh < 240.0) rgb = vec3(0.0, xc, c); |
| 127 | + else if (hh < 300.0) rgb = vec3(xc, 0.0, c); |
| 128 | + else rgb = vec3(c, 0.0, xc); |
| 129 | + return floor((rgb + vec3(m)) * 255.0 + 0.5); |
| 130 | +} |
| 131 | +
|
| 132 | +void main() { |
| 133 | + vec2 px = v_uv * u_res; |
| 134 | + float jsX = floor(px.x); |
| 135 | + float jsY = u_res.y - 1.0 - floor(px.y); |
| 136 | +
|
| 137 | + float value = 0.0; |
| 138 | + float amplitude = 1.0; |
| 139 | + float frequency = 1.0; |
| 140 | + float maxAmp = 0.0; |
| 141 | + for (int o = 0; o < 8; o++) { |
| 142 | + if (o >= u_octaves) break; |
| 143 | + float nx = (jsX / u_scale) * frequency; |
| 144 | + float ny = (jsY / u_scale) * frequency; |
| 145 | + value += sampleNoise(nx, ny, u_seed + o * 1000 + u_frame * 7) * amplitude; |
| 146 | + maxAmp += amplitude; |
| 147 | + amplitude *= 0.5; |
| 148 | + frequency *= 2.0; |
| 149 | + } |
| 150 | +
|
| 151 | + float n = clamp((value / max(1e-6, maxAmp) + 1.0) * 0.5, 0.0, 1.0); |
| 152 | +
|
| 153 | + vec3 noiseRgb; |
| 154 | + if (u_colorize == 1) { |
| 155 | + noiseRgb = hslToRgb(n * 360.0, 0.8, 0.5); |
| 156 | + } else { |
| 157 | + float v = floor(n * 255.0 + 0.5); |
| 158 | + noiseRgb = vec3(v); |
| 159 | + } |
| 160 | +
|
| 161 | + vec4 src = texture(u_source, vec2((jsX + 0.5) / u_res.x, 1.0 - (jsY + 0.5) / u_res.y)); |
| 162 | + vec3 srcRgb = src.rgb * 255.0; |
| 163 | + vec3 outRgb = floor(srcRgb * (1.0 - u_mix) + noiseRgb * u_mix + 0.5); |
| 164 | + fragColor = vec4(clamp(outRgb, 0.0, 255.0) / 255.0, src.a); |
| 165 | +} |
| 166 | +`; |
| 167 | + |
| 168 | +type Cache = { prog: Program }; |
| 169 | +let _cache: Cache | null = null; |
| 170 | +const initCache = (gl: WebGL2RenderingContext): Cache => { |
| 171 | + if (_cache) return _cache; |
| 172 | + _cache = { prog: linkProgram(gl, FS, [ |
| 173 | + "u_source", "u_res", "u_type", "u_scale", "u_octaves", |
| 174 | + "u_seed", "u_frame", "u_colorize", "u_mix", |
| 175 | + ] as const) }; |
| 176 | + return _cache; |
| 177 | +}; |
| 178 | + |
| 179 | +export const noiseGeneratorGLAvailable = (): boolean => glAvailable(); |
| 180 | + |
| 181 | +export type NoiseType = 0 | 1 | 2; |
| 182 | + |
| 183 | +export const renderNoiseGeneratorGL = ( |
| 184 | + source: HTMLCanvasElement | OffscreenCanvas, |
| 185 | + width: number, height: number, |
| 186 | + type: NoiseType, scale: number, octaves: number, |
| 187 | + seed: number, frame: number, colorize: boolean, mix: number, |
| 188 | +): HTMLCanvasElement | OffscreenCanvas | null => { |
| 189 | + const ctx = getGLCtx(); |
| 190 | + if (!ctx) return null; |
| 191 | + const { gl, canvas } = ctx; |
| 192 | + const cache = initCache(gl); |
| 193 | + const vao = getQuadVAO(gl); |
| 194 | + resizeGLCanvas(canvas, width, height); |
| 195 | + const sourceTex = ensureTexture(gl, "noiseGenerator:source", width, height); |
| 196 | + uploadSourceTexture(gl, sourceTex, source); |
| 197 | + drawPass(gl, null, width, height, cache.prog, () => { |
| 198 | + gl.activeTexture(gl.TEXTURE0); |
| 199 | + gl.bindTexture(gl.TEXTURE_2D, sourceTex.tex); |
| 200 | + gl.uniform1i(cache.prog.uniforms.u_source, 0); |
| 201 | + gl.uniform2f(cache.prog.uniforms.u_res, width, height); |
| 202 | + gl.uniform1i(cache.prog.uniforms.u_type, type); |
| 203 | + gl.uniform1f(cache.prog.uniforms.u_scale, scale); |
| 204 | + gl.uniform1i(cache.prog.uniforms.u_octaves, Math.max(1, Math.min(8, Math.round(octaves)))); |
| 205 | + gl.uniform1i(cache.prog.uniforms.u_seed, seed | 0); |
| 206 | + gl.uniform1i(cache.prog.uniforms.u_frame, frame | 0); |
| 207 | + gl.uniform1i(cache.prog.uniforms.u_colorize, colorize ? 1 : 0); |
| 208 | + gl.uniform1f(cache.prog.uniforms.u_mix, mix); |
| 209 | + }, vao); |
| 210 | + return readoutToCanvas(canvas, width, height); |
| 211 | +}; |
0 commit comments