瀏覽代碼

Initial commit.

SimonDev 2 年之前
父節點
當前提交
692ebe27c2

+ 9 - 0
base.css

@@ -0,0 +1,9 @@
+body {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>SimonDev Raymarching</title>
+  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+  <link rel="stylesheet" type="text/css" href="base.css">
+  <link rel="shortcut icon" href="#">
+</head>
+<body>
+  <script src="./main.js" type="module"></script>
+</body>
+</html>

+ 432 - 0
main.js

@@ -0,0 +1,432 @@
+import * as THREE from 'https://cdn.skypack.dev/[email protected]';
+
+
+class CloudGeneratorAtlas {
+  constructor() {
+  }
+
+  async init_(threejs) {
+    this.create_();
+    this.onLoad = () => {};
+
+    this.threejs_ = threejs;
+
+    const header = await fetch('./shaders/header.glsl');
+    const common = await fetch('./shaders/common.glsl');
+    const oklab = await fetch('./shaders/oklab.glsl');
+    const blends = await fetch('./shaders/blend-modes.glsl');
+    const noise = await fetch('./shaders/noise.glsl');
+    const vsh = await fetch('./shaders/vertex-shader.glsl');
+    const fsh = await fetch('./shaders/generator-shader.glsl');
+  
+    const material = new THREE.ShaderMaterial({
+      uniforms: {
+        zLevel: { value: 0.0 },
+        numCells: { value: 2.0 },
+      },
+      vertexShader: await vsh.text(),
+      fragmentShader: (
+        await header.text() + '\n' +
+        await oklab.text() + '\n' +
+        await common.text() + '\n' +
+        await blends.text() + '\n' +
+        await noise.text() + '\n' +
+        await fsh.text())
+    });
+
+    this.material_ = material;
+    this.scene_ = new THREE.Scene();
+    this.camera_ = new THREE.OrthographicCamera(0, 1, 1, 0, 0.1, 1000);
+    this.camera_.position.set(0, 0, 1);
+  
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    const plane = new THREE.Mesh(geometry, material);
+    plane.position.set(0.5, 0.5, 0);
+    this.scene_.add(plane);
+
+    this.resolution_ = 32;
+    this.rt_ = new THREE.WebGLRenderTarget(this.resolution_, this.resolution_);
+  }
+
+  create_() {
+    this.manager_ = new THREE.LoadingManager();
+    this.loader_ = new THREE.TextureLoader(this.manager_);
+    this.textures_ = {};
+
+    this.manager_.onLoad = () => {
+      this.onLoad_();
+    };
+  }
+
+  get Info() {
+    return this.textures_;
+  }
+
+  render_(t) {
+    this.material_.uniforms.zLevel.value = t;
+
+    this.threejs_.setRenderTarget(this.rt_);
+    this.threejs_.render(this.scene_, this.camera_);
+  
+    const pixelBuffer = new Uint8Array(this.resolution_ * this.resolution_ * 4);
+    this.threejs_.readRenderTargetPixels(this.rt_, 0, 0, this.resolution_, this.resolution_, pixelBuffer );
+    this.threejs_.setRenderTarget(null);
+    this.threejs_.outputEncoding = THREE.LinearEncoding;
+    return pixelBuffer;
+  }
+
+  onLoad_() {
+    const X = this.resolution_;
+    const Y = this.resolution_;
+
+    this.textures_['diffuse'] = {
+      textures: []
+    };
+    for (let i = 0; i < 64; i++) {
+      this.textures_['diffuse'].textures.push(this.render_(i / 4.0));
+    }
+
+    for (let k in this.textures_) {
+      const atlas = this.textures_[k];
+      const data = new Uint8Array(atlas.textures.length * 4 * X * Y);
+
+      for (let t = 0; t < atlas.textures.length; t++) {
+        const curData = atlas.textures[t];
+        const offset = t * (4 * X * Y);
+
+        data.set(curData, offset);
+      }
+
+      // const diffuse = new THREE.DataArrayTexture(data, X, Y, atlas.textures.length);
+      const diffuse = new THREE.Data3DTexture(data, X, Y, atlas.textures.length);
+      diffuse.format = THREE.RGBAFormat;
+      diffuse.type = THREE.UnsignedByteType;
+      diffuse.minFilter = THREE.LinearFilter;
+      diffuse.magFilter = THREE.LinearFilter;
+      // diffuse.anisotropy = this.threejs_.capabilities.getMaxAnisotropy();
+      diffuse.wrapS = THREE.RepeatWrapping;
+      diffuse.wrapT = THREE.RepeatWrapping;
+      diffuse.wrapR = THREE.RepeatWrapping;
+      diffuse.generateMipmaps = true;
+      diffuse.needsUpdate = true;
+
+      atlas.atlas = diffuse;
+    }
+
+    this.onLoad();
+  }
+
+  Load() {
+    this.onLoad_();
+  }
+}
+
+class SDFFieldGenerator {
+  constructor() {
+  }
+
+  async init_(threejs) {
+    this.create_();
+    this.onLoad = () => {};
+
+    this.threejs_ = threejs;
+
+    const header = await fetch('./shaders/header.glsl');
+    const common = await fetch('./shaders/common.glsl');
+    const oklab = await fetch('./shaders/oklab.glsl');
+    const blends = await fetch('./shaders/blend-modes.glsl');
+    const noise = await fetch('./shaders/noise.glsl');
+    const vsh = await fetch('./shaders/vertex-shader.glsl');
+    const fsh = await fetch('./shaders/sdf-generator-shader.glsl');
+  
+    const material = new THREE.ShaderMaterial({
+      uniforms: {
+        zLevel: { value: 0.0 },
+        numCells: { value: 2.0 },
+      },
+      vertexShader: await vsh.text(),
+      fragmentShader: (
+        await header.text() + '\n' +
+        await oklab.text() + '\n' +
+        await common.text() + '\n' +
+        await blends.text() + '\n' +
+        await noise.text() + '\n' +
+        await fsh.text())
+    });
+
+    this.material_ = material;
+    this.scene_ = new THREE.Scene();
+    this.camera_ = new THREE.OrthographicCamera(0, 1, 1, 0, 0.1, 1000);
+    this.camera_.position.set(0, 0, 1);
+  
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    const plane = new THREE.Mesh(geometry, material);
+    plane.position.set(0.5, 0.5, 0);
+    this.scene_.add(plane);
+
+    this.resolution_ = 128;
+    this.rt_ = new THREE.WebGLRenderTarget(this.resolution_, this.resolution_);
+  }
+
+  create_() {
+    this.manager_ = new THREE.LoadingManager();
+    this.loader_ = new THREE.TextureLoader(this.manager_);
+    this.textures_ = {};
+
+    this.manager_.onLoad = () => {
+      this.onLoad_();
+    };
+  }
+
+  get Info() {
+    return this.textures_;
+  }
+
+  render_(t) {
+    this.material_.uniforms.zLevel.value = t;
+
+    this.threejs_.setRenderTarget(this.rt_);
+    this.threejs_.render(this.scene_, this.camera_);
+  
+    const pixelBuffer = new Uint8Array(this.resolution_ * this.resolution_ * 4);
+    this.threejs_.readRenderTargetPixels(this.rt_, 0, 0, this.resolution_, this.resolution_, pixelBuffer );
+    this.threejs_.setRenderTarget(null);
+    this.threejs_.outputEncoding = THREE.LinearEncoding;
+    return pixelBuffer;
+  }
+
+  onLoad_() {
+    const X = this.resolution_;
+    const Y = this.resolution_;
+    const CHANNELS = 4;
+
+    this.textures_['diffuse'] = {
+      textures: []
+    };
+    for (let i = 0; i < 32; i++) {
+      this.textures_['diffuse'].textures.push(this.render_(i));
+    }
+
+    for (let k in this.textures_) {
+      const atlas = this.textures_[k];
+      const data = new Uint8Array(atlas.textures.length * CHANNELS * X * Y);
+
+      for (let t = 0; t < atlas.textures.length; t++) {
+        const curData = atlas.textures[t];
+        const offset = t * (CHANNELS * X * Y);
+
+        data.set(curData, offset);
+      }
+
+      // const diffuse = new THREE.DataArrayTexture(data, X, Y, atlas.textures.length);
+      const diffuse = new THREE.Data3DTexture(data, X, Y, atlas.textures.length);
+      diffuse.format = THREE.RGBAFormat;
+      diffuse.type = THREE.UnsignedByteType;
+      diffuse.minFilter = THREE.LinearFilter;
+      diffuse.magFilter = THREE.LinearFilter;
+      diffuse.anisotropy = this.threejs_.capabilities.getMaxAnisotropy();
+      diffuse.wrapS = THREE.RepeatWrapping;
+      diffuse.wrapT = THREE.RepeatWrapping;
+      diffuse.wrapR = THREE.ClampToEdgeWrapping;
+      diffuse.generateMipmaps = true;
+      diffuse.needsUpdate = true;
+
+      atlas.atlas = diffuse;
+    }
+
+    this.onLoad();
+  }
+
+  Load() {
+    this.onLoad_();
+  }
+}
+
+
+class SimonDev {
+  constructor() {
+  }
+
+  async initialize() {
+    this.threejs_ = new THREE.WebGLRenderer();
+    this.threejs_.outputEncoding = THREE.LinearEncoding;
+    document.body.appendChild(this.threejs_.domElement);
+
+    window.addEventListener('resize', () => {
+      this.onWindowResize_();
+    }, false);
+
+    this.scene_ = new THREE.Scene();
+    this.finalScene_ = new THREE.Scene();
+
+    this.camera_ = new THREE.OrthographicCamera(0, 1, 1, 0, 0.1, 1000);
+    this.camera_.position.set(0, 0, 1);
+
+    this.rtScale_ = 0.5;
+    this.rtParams_ = {
+        type: THREE.HalfFloatType,
+        magFilter: THREE.LinearFilter,
+        minFilter: THREE.LinearMipmapLinearFilter,
+        anisotropy: this.threejs_.capabilities.getMaxAnisotropy(),
+        generateMipmaps: true,
+    };
+    this.rt_ = new THREE.WebGLRenderTarget(
+        window.innerWidth * this.rtScale_, window.innerHeight * this.rtScale_, this.rtParams_);
+
+    await this.setupProject_();
+  }
+
+  async setupProject_() {
+    
+    {
+      const header = await fetch('./shaders/header.glsl');
+      const common = await fetch('./shaders/common.glsl');
+      const oklab = await fetch('./shaders/oklab.glsl');
+      const blends = await fetch('./shaders/blend-modes.glsl');
+      const noise = await fetch('./shaders/noise.glsl');
+      const ui2d = await fetch('./shaders/ui2d.glsl');
+      const vsh = await fetch('./shaders/vertex-shader.glsl');
+      const fshComposite = await fetch('./shaders/fragment-shader-composite.glsl');
+
+      const compositeMaterial = new THREE.ShaderMaterial({
+        uniforms: {
+          resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
+          frameResolution: { value: new THREE.Vector2(this.rt_.width, this.rt_.height) },
+          time: { value: 0.0 },
+          frameTexture: { value: null },
+          uiTextures: { value: null },
+        },
+        vertexShader: await vsh.text(),
+        fragmentShader: (
+          await header.text() + '\n' +
+          await oklab.text() + '\n' +
+          await common.text() + '\n' +
+          await blends.text() + '\n' +
+          await noise.text() + '\n' +
+          await ui2d.text() + '\n' +
+          await fshComposite.text())
+      });
+  
+      this.compositeMaterial_ = compositeMaterial;
+      const geometry = new THREE.PlaneGeometry(1, 1);
+      const plane = new THREE.Mesh(geometry, compositeMaterial);
+      plane.position.set(0.5, 0.5, 0);
+      this.finalScene_.add(plane);
+    }
+
+    const header = await fetch('./shaders/header.glsl');
+    const common = await fetch('./shaders/common.glsl');
+    const oklab = await fetch('./shaders/oklab.glsl');
+    const blends = await fetch('./shaders/blend-modes.glsl');
+    const noise = await fetch('./shaders/noise.glsl');
+    const ui2d = await fetch('./shaders/ui2d.glsl');
+    const vsh = await fetch('./shaders/vertex-shader.glsl');
+    const fsh = await fetch('./shaders/fragment-shader.glsl');
+
+
+    const loader = new THREE.TextureLoader();
+    const blueNoise = loader.load('./textures/HDR_L_0.png');
+
+    blueNoise.wrapS = THREE.RepeatWrapping;
+    blueNoise.wrapT = THREE.RepeatWrapping;
+    blueNoise.minFilter = THREE.NearestFilter;
+    blueNoise.magFilter = THREE.NearestFilter;
+
+    const material = new THREE.ShaderMaterial({
+      uniforms: {
+        resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
+        time: { value: 0.0 },
+        frame: { value: 0 },
+        perlinWorley: { value: null },
+        sdfField: { value: null },
+        blueNoise: { value: blueNoise },
+      },
+      vertexShader: await vsh.text(),
+      fragmentShader: (
+        await header.text() + '\n' +
+        await oklab.text() + '\n' +
+        await common.text() + '\n' +
+        await blends.text() + '\n' +
+        await noise.text() + '\n' +
+        await ui2d.text() + '\n' +
+        await fsh.text())
+    });
+
+    const diffuse = new CloudGeneratorAtlas(this.threejs_);
+    await diffuse.init_(this.threejs_);
+    diffuse.onLoad = () => {     
+      material.uniforms.perlinWorley.value = diffuse.Info['diffuse'].atlas;
+    };
+    diffuse.Load();
+
+    const sdfTextureGenerator = new SDFFieldGenerator(this.threejs_);
+    await sdfTextureGenerator.init_(this.threejs_);
+    sdfTextureGenerator.onLoad = () => {     
+      material.uniforms.sdfField.value = sdfTextureGenerator.Info['diffuse'].atlas;
+    };
+    sdfTextureGenerator.Load();
+
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    const plane = new THREE.Mesh(geometry, material);
+    plane.position.set(0.5, 0.5, 0);
+    this.scene_.add(plane);
+
+    this.material_ = material;
+
+    this.clock_ = new THREE.Clock();
+    this.totalTime_ = 0;
+    this.previousRAF_ = null;
+    this.onWindowResize_();
+    this.raf_();  
+
+  }
+
+  onWindowResize_() {
+    this.threejs_.setSize(window.innerWidth, window.innerHeight);
+    this.rt_.setSize(window.innerWidth * this.rtScale_, window.innerHeight * this.rtScale_);
+    this.material_.uniforms.resolution.value = new THREE.Vector2(this.rt_.width, this.rt_.height);
+    this.compositeMaterial_.uniforms.resolution.value = new THREE.Vector2(window.innerWidth, window.innerHeight);
+    this.compositeMaterial_.uniforms.frameResolution.value = new THREE.Vector2(this.rt_.width, this.rt_.height);
+  }
+
+  raf_() {
+    requestAnimationFrame((t) => {
+      this.step_(t - this.previousRAF_);
+      this.render_();
+
+      if (!this.clock_.running) {
+        this.clock_.start();
+      }
+
+      setTimeout(() => {
+        this.raf_();
+        this.previousRAF_ = t;
+      }, 1);
+    });
+  }
+
+  step_(timeElapsed) {
+    this.totalTime_ = this.clock_.getElapsedTime();
+
+    this.material_.uniforms.time.value = this.totalTime_;
+    this.material_.uniforms.frame.value = this.material_.uniforms.frame.value + 1;
+    this.compositeMaterial_.uniforms.time.value = this.totalTime_;
+  }
+
+  render_() {
+    this.threejs_.setRenderTarget(this.rt_);
+    this.threejs_.render(this.scene_, this.camera_);
+
+    this.threejs_.setRenderTarget(null);
+    this.compositeMaterial_.uniforms.frameTexture.value = this.rt_.texture;
+    this.threejs_.render(this.finalScene_, this.camera_);
+  }
+}
+
+
+let APP_ = null;
+
+window.addEventListener('DOMContentLoaded', async () => {
+  APP_ = new SimonDev();
+  await APP_.initialize();
+});

+ 0 - 0
shaders/blend-modes.glsl


+ 188 - 0
shaders/common.glsl

@@ -0,0 +1,188 @@
+#define PI 3.14159265359
+
+
+float saturate(float x) {
+  return clamp(x, 0.0, 1.0);
+}
+
+vec3 saturate3(vec3 x) {
+  return clamp(x, vec3(0.0), vec3(1.0));
+}
+
+
+float linearstep(float minValue, float maxValue, float v) {
+  return clamp((v - minValue) / (maxValue - minValue), 0.0, 1.0);
+}
+
+float inverseLerp(float minValue, float maxValue, float v) {
+  return (v - minValue) / (maxValue - minValue);
+}
+
+float inverseLerpSat(float minValue, float maxValue, float v) {
+  return saturate((v - minValue) / (maxValue - minValue));
+}
+
+float remap(float v, float inMin, float inMax, float outMin, float outMax) {
+  float t = inverseLerp(inMin, inMax, v);
+  return mix(outMin, outMax, t);
+}
+
+float smootherstep(float edge0, float edge1, float x) {
+  x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+  return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+vec2 smootherstep2(vec2 edge0, vec2 edge1, vec2 x) {
+  x = clamp((x - edge0) / (edge1 - edge0), vec2(0.0), vec2(1.0));
+  return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+vec3 smootherstep3(vec3 edge0, vec3 edge1, vec3 x) {
+  x = clamp((x - edge0) / (edge1 - edge0), vec3(0.0), vec3(1.0));
+  return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+float circularOut(float t) {
+  return sqrt((2.0 - t) * t);
+}
+
+/////////////////////////////////////////////////////////////////////////
+//
+// 3D SDF's
+//
+/////////////////////////////////////////////////////////////////////////
+
+// https://iquilezles.org/articles/distfunctions/
+
+float sdfSphere(vec3 p, float r) {
+  return length(p) - r;
+}
+
+float sdfBox( vec3 p, vec3 b ) {
+  vec3 q = abs(p) - b;
+  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
+}
+
+float sdCutSphere( vec3 p, float r, float h )
+{
+  // sampling independent computations (only depend on shape)
+  float w = sqrt(r*r-h*h);
+
+  // sampling dependant computations
+  vec2 q = vec2( length(p.xz), p.y );
+  float s = max( (h-r)*q.x*q.x+w*w*(h+r-2.0*q.y), h*q.x-w*q.y );
+  return (s<0.0) ? length(q)-r :
+         (q.x<w) ? h - q.y     :
+                   length(q-vec2(w,h));
+}
+
+/////////////////////////////////////////////////////////////////////////
+//
+// Misc
+//
+/////////////////////////////////////////////////////////////////////////
+vec3 vignette(vec2 uvs) {
+  float v1 = smoothstep(0.5, 0.3, abs(uvs.x - 0.5));
+  float v2 = smoothstep(0.5, 0.3, abs(uvs.y - 0.5));
+  float v = v1 * v2;
+  v = pow(v, 0.25);
+  v = remap(v, 0.0, 1.0, 0.4, 1.0);
+  return vec3(v);
+}
+
+
+/////////////////////////////////////////////////////////////////////////
+//
+// ToneMapping Operators
+//
+/////////////////////////////////////////////////////////////////////////
+vec3 ACESToneMap(vec3 color){	
+	mat3 m1 = mat3(
+        0.59719, 0.07600, 0.02840,
+        0.35458, 0.90834, 0.13383,
+        0.04823, 0.01566, 0.83777
+	);
+	mat3 m2 = mat3(
+        1.60475, -0.10208, -0.00327,
+        -0.53108,  1.10813, -0.07276,
+        -0.07367, -0.00605,  1.07602
+	);
+	vec3 v = m1 * color;    
+	vec3 a = v * (v + 0.0245786) - 0.000090537;
+	vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
+	return saturate3(m2 * (a / b));	
+}
+
+/////////////////////////////////////////////////////////////////////////
+//
+// Intersections
+//
+/////////////////////////////////////////////////////////////////////////
+
+// Return the near and far intersections of an infinite ray and a sphere. 
+// Assumes sphere at origin. No intersection if result.x > result.y
+vec2 sphereIntersections(vec3 start, vec3 dir, float radius){
+	float a = dot(dir, dir);
+	float b = 2.0 * dot(dir, start);
+    float c = dot(start, start) - (radius * radius);
+	float d = (b*b) - 4.0*a*c;
+	if (d < 0.0){
+        return vec2(1e5, -1e5);
+	}
+	return vec2((-b - sqrt(d))/(2.0*a), (-b + sqrt(d))/(2.0*a));
+}
+
+struct AABB {
+  vec3 min;
+  vec3 max;
+};
+
+struct AABBIntersectResult {
+  float near;
+  float far;
+};
+
+bool insideAABB(vec3 rayOrigin, AABB box) {
+  return all(lessThanEqual(rayOrigin, box.max)) && all(lessThan(box.min, rayOrigin));
+}
+
+// https://gist.github.com/DomNomNom/46bb1ce47f68d255fd5d
+// Compute the near and far intersections using the slab method.
+// No intersection if tNear > tFar.
+AABBIntersectResult intersectAABB(vec3 rayOrigin, vec3 rayDir, AABB box) {
+    vec3 tMin = (box.min - rayOrigin) / rayDir;
+    vec3 tMax = (box.max - rayOrigin) / rayDir;
+    vec3 t1 = min(tMin, tMax);
+    vec3 t2 = max(tMin, tMax);
+    float tNear = max(max(t1.x, t1.y), t1.z);
+    float tFar = min(min(t2.x, t2.y), t2.z);
+    return AABBIntersectResult(tNear, tFar);
+}
+
+struct AABB2D {
+  vec2 min;
+  vec2 max;
+};
+
+struct AABB2DIntersectResult {
+  float near;
+  float far;
+};
+
+bool insideAABB(vec2 rayOrigin, AABB2D box) {
+  return all(lessThanEqual(rayOrigin, box.max)) && all(lessThan(box.min, rayOrigin));
+}
+
+// https://gist.github.com/DomNomNom/46bb1ce47f68d255fd5d
+// Compute the near and far intersections using the slab method.
+// No intersection if tNear > tFar.
+AABB2DIntersectResult intersectAABB2D(vec2 rayOrigin, vec2 rayDir, AABB2D box) {
+    vec2 tMin = (box.min - rayOrigin) / rayDir;
+    vec2 tMax = (box.max - rayOrigin) / rayDir;
+    vec2 t1 = min(tMin, tMax);
+    vec2 t2 = max(tMin, tMax);
+    float tNear = max(t1.x, t1.y);
+    float tFar = min(t2.x, t2.y);
+    return AABB2DIntersectResult(tNear, tFar);
+}
+

+ 24 - 0
shaders/fragment-shader-composite.glsl

@@ -0,0 +1,24 @@
+
+
+uniform sampler2D frameTexture;
+uniform vec2 frameResolution;
+
+
+void main() {
+  vec2 pixelCoords = (vUvs - 0.5) * resolution;
+
+  float curTime = time*TIME_SPEED + TIME_OFFSET;
+
+  vec4 colour = texture(frameTexture, vUvs);
+
+#ifdef USE_OKLAB
+  colour.xyz = oklabToRGB(colour.xyz);
+#endif
+
+  // Vignette
+  colour.xyz *= vignette(vUvs);
+  colour.xyz = pow(saturate3(colour.xyz), vec3(1.0 / 2.2));
+
+  gl_FragColor = vec4(colour.xyz, 1.0);
+}
+

+ 331 - 0
shaders/fragment-shader.glsl

@@ -0,0 +1,331 @@
+const float GOLDEN_RATIO = 1.61803398875;
+const vec3 EXTINCTION_MULT = vec3(0.8, 0.8, 1.0);
+
+const float DUAL_LOBE_WEIGHT = 0.7;
+const float AMBIENT_STRENGTH = 0.1;
+
+const float CLOUD_LIGHT_MULTIPLIER = 50.0;
+const vec3 CLOUD_LIGHT_DIR = normalize(vec3(-1.0, 0.0, 0.0));
+
+const float CLOUD_EXPOSURE = 1.0;
+const float CLOUD_STEPS_MIN = 16.0;
+const float CLOUD_STEPS_MAX = 128.0;
+const float CLOUD_LIGHT_STEPS = 12.0;
+const float CLOUD_DENSITY = 0.5;
+
+const vec3 CLOUD_OFFSET = vec3(0.0, 0.0, 0.0);
+
+vec3 CLOUD_SIZE = vec3(4000.0);
+vec3 CLOUD_BOUNDS_MIN;
+vec3 CLOUD_BOUNDS_MAX;
+
+const float CLOUD_BASE_STRENGTH = 0.8;
+const float CLOUD_DETAIL_STRENGTH = 0.2;
+
+const vec3 CLOUD_COLOUR = vec3(1.0);
+const float CLOUD_FALLOFF = 25.0;
+
+// #define SHOW_CLOUD_MAP
+
+
+float HenyeyGreenstein(float g, float mu) {
+  float gg = g * g;
+	return (1.0 / (4.0 * PI))  * ((1.0 - gg) / pow(1.0 + gg - 2.0 * g * mu, 1.5));
+}
+
+float IsotropicPhaseFunction(float g, float costh) {
+  return 1.0 / (4.0 * PI);
+}
+
+float DualHenyeyGreenstein(float g, float costh) {
+  return mix(HenyeyGreenstein(-g, costh), HenyeyGreenstein(g, costh), DUAL_LOBE_WEIGHT);
+}
+
+float PhaseFunction(float g, float costh) {
+  return DualHenyeyGreenstein(g, costh);
+}
+
+
+vec4 SamplePerlinWorleyNoise(vec3 pos) {
+  vec3 coord = pos.xzy * vec3(1.0 / 32.0, 1.0 / 32.0, 1.0 / 64.0) * 1.0;
+  vec4 s = texture(perlinWorley, coord);
+
+  return s;
+}
+
+
+float SampleLowResolutionCloudMap(vec3 p) {
+  float sdfValue = sdfBox(p, vec3(50.0));
+
+  sdfValue = sdCutSphere(p, 60.0, -40.0);
+  sdfValue = min(sdfValue, sdCutSphere(p - vec3(60.0, -20.0, 0.0), 40.0, -20.0));
+  sdfValue = min(sdfValue, sdCutSphere(p - vec3(-60.0, -20.0, -50.0), 20.0, -20.0));
+
+  return sdfValue;
+}
+
+float SampleHighResolutionCloudDetail(float cloudSDF, vec3 worldPos, vec3 cameraOrigin, float curTime) {
+  float cloud = circularOut(linearstep(0.0, -CLOUD_FALLOFF, cloudSDF)) * 0.85;
+
+  if(cloud > 0.0){
+    vec3 samplePos = worldPos + vec3(-2.0 * curTime, 0.0, curTime) * 1.5;
+
+    float shapeSize = 0.4;
+    vec4 perlinWorleySample = SamplePerlinWorleyNoise(samplePos * shapeSize);
+
+    float shapeStrength = CLOUD_BASE_STRENGTH;
+    cloud = saturate(remap(cloud, shapeStrength * perlinWorleySample.x, 1.0, 0.0, 1.0));
+
+    if(cloud > 0.0){
+      float distToSample = distance(cameraOrigin, worldPos);
+      float t_detailDropout = smoothstep(1000.0, 800.0, distToSample);
+
+      if (t_detailDropout > 0.0) {
+        samplePos += vec3(4.0 * curTime, 3.0 * curTime, 2.0 * curTime) * 0.01;
+
+        float detailSize = 1.8;
+        float detailStrength = CLOUD_DETAIL_STRENGTH * t_detailDropout;
+        float detail = SamplePerlinWorleyNoise(detailSize * samplePos).y;
+        cloud = saturate(remap(cloud, detailStrength * detail, 1.0, 0.0, 1.0));
+      }
+    }
+  }
+
+  return cloud * CLOUD_DENSITY;
+}
+
+
+// Adapted from: https://twitter.com/FewesW/status/1364629939568451587/photo/1
+vec3 MultipleOctaveScattering(float density, float mu) {
+  float attenuation = 0.2;
+  float contribution = 0.2;
+  float phaseAttenuation = 0.5;
+
+  float a = 1.0;
+  float b = 1.0;
+  float c = 1.0;
+  float g = 0.85;
+  const float scatteringOctaves = 4.0;
+  
+  vec3 luminance = vec3(0.0);
+
+  for (float i = 0.0; i < scatteringOctaves; i++) {
+      float phaseFunction = PhaseFunction(0.3 * c, mu);
+      vec3 beers = exp(-density * EXTINCTION_MULT * a);
+
+      luminance += b * phaseFunction * beers;
+
+      a *= attenuation;
+      b *= contribution;
+      c *= (1.0 - phaseAttenuation);
+  }
+  return luminance;
+}
+
+
+vec3 CalculateLightEnergy(
+    vec3 lightOrigin, vec3 lightDirection, vec3 cameraOrigin, float mu, float maxDistance, float curTime) {
+  float stepLength = maxDistance / CLOUD_LIGHT_STEPS;
+	float lightRayDensity = 0.0;
+  float distAccumulated = 0.0;
+
+  for(float j = 0.0; j < CLOUD_LIGHT_STEPS; j++) {
+    vec3 lightSamplePos = lightOrigin + lightDirection * distAccumulated;
+	
+    float cloudSDF = SampleLowResolutionCloudMap(lightSamplePos);
+
+    lightRayDensity += SampleHighResolutionCloudDetail(cloudSDF, lightSamplePos, cameraOrigin, curTime) * stepLength;
+    distAccumulated += stepLength;
+  }
+
+	vec3 beersLaw = MultipleOctaveScattering(lightRayDensity, mu);
+  vec3 powder = 1.0 - exp(-lightRayDensity * 2.0 * EXTINCTION_MULT);
+
+	return beersLaw * mix(2.0 * powder, vec3(1.0), remap(mu, -1.0, 1.0, 0.0, 1.0));
+}
+
+struct ScatteringTransmittance {
+  vec3 scattering;
+  vec3 transmittance;
+};
+
+ScatteringTransmittance CloudMarch(vec2 pixelCoords, vec3 cameraOrigin, vec3 cameraDirection, float curTime) {
+  AABB cloudAABB;
+  cloudAABB.min = CLOUD_BOUNDS_MIN;
+  cloudAABB.max = CLOUD_BOUNDS_MAX;
+
+  ScatteringTransmittance result;
+  result.scattering = vec3(0.0);
+  result.transmittance = vec3(1.0);
+
+  AABBIntersectResult rayCloudIntersection = intersectAABB(cameraOrigin, cameraDirection, cloudAABB);
+  if (rayCloudIntersection.near >= rayCloudIntersection.far) {
+    // Debug
+    // return vec4(vec3(0.0), 0.0);
+    return result;
+  }
+
+  if (insideAABB(cameraOrigin, cloudAABB)) {
+    rayCloudIntersection.near = 0.0;
+  }
+
+  vec3 sunDirection = CLOUD_LIGHT_DIR;
+  vec3 sunLightColour = vec3(1.0);
+  vec3 sunLight = sunLightColour * CLOUD_LIGHT_MULTIPLIER;
+  vec3 ambient = vec3(AMBIENT_STRENGTH * sunLightColour);
+
+  // TODO: Cap steps based on distance
+  vec2 aspect = vec2(1.0, resolution.y / resolution.x);
+  float blueNoiseSample = texture2D(blueNoise, (pixelCoords / resolution + 0.5) * aspect * (resolution.x / 32.0)).x;
+
+  // Animating Noise For Integration Over Time
+  // https://blog.demofox.org/2017/10/31/animating-noise-for-integration-over-time/
+  blueNoiseSample = fract(blueNoiseSample + float(frame % 32) * GOLDEN_RATIO);
+
+  float mu = dot(cameraDirection, sunDirection);
+	float phaseFunction = PhaseFunction(0.3, mu);
+
+  float distNearToFar = rayCloudIntersection.far - rayCloudIntersection.near;
+  float stepDropoff = linearstep(1.0, 0.0, pow(dot(vec3(0.0, 1.0, 0.0), cameraDirection), 4.0));
+
+  const int NUM_COUNT = 16;
+  float lqStepLength = distNearToFar / CLOUD_STEPS_MIN; 
+  float hqStepLength = lqStepLength / float(NUM_COUNT);
+  float numCloudSteps = CLOUD_STEPS_MAX;
+
+  float offset = lqStepLength * blueNoiseSample;
+  float distTravelled = rayCloudIntersection.near;
+
+  int hqMarcherCountdown = 0;
+
+  float previousStepLength = 0.0;
+
+	for (float i = 0.0; i < numCloudSteps; i++) {
+    if (distTravelled > rayCloudIntersection.far) {
+      break;
+    }
+
+    vec3 samplePos = cameraOrigin + cameraDirection * distTravelled;
+    float cloudMapSDFSample = SampleLowResolutionCloudMap(samplePos);
+
+    float currentStepLength = cloudMapSDFSample;
+    // float currentStepLength = lqStepLength;
+
+    if (hqMarcherCountdown <= 0) {
+      if (cloudMapSDFSample < hqStepLength) {
+        // Hit some clouds, step back
+        // distTravelled = (distTravelled - currentStepLength) + hqStepLength;
+        hqMarcherCountdown = NUM_COUNT;
+
+        distTravelled += hqStepLength * blueNoiseSample;
+
+      } else {
+        distTravelled += currentStepLength;
+        continue;
+      }
+    }
+
+    if (hqMarcherCountdown > 0) {
+      hqMarcherCountdown--;
+
+      if (cloudMapSDFSample < 0.0) {
+        hqMarcherCountdown = NUM_COUNT;
+
+        float extinction = SampleHighResolutionCloudDetail(cloudMapSDFSample, samplePos, cameraOrigin, curTime);
+
+        if (extinction > 0.01) {
+          vec3 luminance = ambient + sunLight * CalculateLightEnergy(samplePos, sunDirection, cameraOrigin, mu, 50.0, curTime);
+          vec3 transmittance = exp(-extinction * hqStepLength * EXTINCTION_MULT);
+          vec3 integScatt = extinction * (luminance - luminance * transmittance) / extinction;
+
+          result.scattering += result.transmittance * integScatt;
+          result.transmittance *= transmittance;  
+
+          if (length(result.transmittance) <= 0.01) {
+            result.transmittance = vec3(0.0);
+            break;
+          }
+        }
+      }
+
+      distTravelled += hqStepLength;
+    }
+
+    previousStepLength = currentStepLength;
+	}
+
+  result.scattering = col3(result.scattering) * CLOUD_COLOUR;
+  result.transmittance = saturate3(result.transmittance);
+  return result;
+}
+
+
+float RenderGlow(float dist, float radius, float intensity){
+  dist = max(dist, 1e-6);
+	return (1.0 - exp(-25.0 * (radius / dist))) * 0.1 + (1.0 - exp(-0.05 * (radius / dist) * (radius / dist))) * 2.0;
+}
+
+vec4 RenderSky(vec3 cameraOrigin, vec3 cameraDir, float curTime) {
+  vec3 pos;
+  float skyT1 = pow(smoothstep(0.0, 1.0, vUvs.y), 0.5);
+  float skyT2 = pow(smoothstep(0.5, 1.0, vUvs.y), 1.0);
+
+  vec3 c1 = col3(COLOUR_LIGHT_BLUE * 0.25);
+  vec3 c2 = col3(COLOUR_BRIGHT_BLUE);
+  vec3 c3 = col3(COLOUR_BRIGHT_BLUE * 1.25);
+  vec3 sky = mix(c1, c2, skyT1);
+  sky = mix(sky, c3, skyT2);
+
+  float mu = remap(dot(cameraDir, CLOUD_LIGHT_DIR), -1.0, 1.0, 1.0, 0.0);
+  float glow = RenderGlow(mu, 0.001, 0.5);
+
+  sky += col3(glow, glow, 0.0);
+
+  vec4 result = vec4(sky, 0.0);
+  return result;
+}
+
+
+mat3 MakeCamera(vec3 ro, vec3 rd, vec3 ru) {
+	vec3 z = normalize(rd - ro);
+	vec3 cp = ru;
+	vec3 x = normalize(cross(z, cp));
+	vec3 y = cross(x, z);
+  return mat3(x, y, z);
+}
+
+
+void main() {
+  vec2 pixelCoords = (vUvs - 0.5) * resolution;
+  float curTime = time * TIME_SPEED + TIME_OFFSET;
+
+  CLOUD_SIZE = vec3(100.0);
+  CLOUD_BOUNDS_MIN = CLOUD_OFFSET - CLOUD_SIZE;
+  CLOUD_BOUNDS_MAX = CLOUD_OFFSET + CLOUD_SIZE;
+
+  vec3 rayOrigin = vec3(200.0, 50.0, -150.0) * 0.75;
+  vec3 rayLookAt = vec3(80.0, -10.0, 45.0) + CLOUD_LIGHT_DIR * 150.0;
+  mat3 camera = MakeCamera(rayOrigin, rayLookAt, vec3(0.0, 1.0, 0.0));
+
+  vec2 rayCoords = (2.0 * (gl_FragCoord.xy - 0.5) - resolution) / resolution.y;
+  vec3 rayDir = normalize(vec3(rayCoords, 2.0));
+
+  vec4 pixel = RenderSky(rayOrigin, camera * rayDir, curTime);
+ 
+  ScatteringTransmittance scatterTransmittance = CloudMarch(
+      pixelCoords, rayOrigin, normalize(camera * rayDir), curTime);
+
+  vec3 colour;
+
+#ifdef USE_OKLAB
+  colour = oklabToRGB(pixel.xyz) * scatterTransmittance.transmittance + oklabToRGB(scatterTransmittance.scattering) * CLOUD_EXPOSURE;
+  colour = ACESToneMap(colour);
+  colour = col3(colour);
+#else
+  colour = pixel.xyz * scatterTransmittance.transmittance + scatterTransmittance.scattering * CLOUD_EXPOSURE;
+  colour = ACESToneMap(colour);
+#endif
+
+  gl_FragColor = vec4(colour, pixel.w);
+}
+

+ 133 - 0
shaders/generator-shader.glsl

@@ -0,0 +1,133 @@
+
+uniform float zLevel;
+uniform float numCells;
+
+vec3 hash3_New(vec3 p, float tileLength) {
+  p = mod(p, vec3(tileLength));
+	p = vec3(
+      dot(p,vec3(127.1,311.7, 74.7)),
+			dot(p,vec3(269.5,183.3,246.1)),
+			dot(p,vec3(113.5,271.9,124.6)));
+
+	return -1.0 + 2.0*fract(sin(p)*43758.5453123);
+}
+
+float voronoiSlow(float maxOffset, float cellsMult, float seed) {
+  vec3 coords = vec3(vUvs * cellsMult, zLevel);
+  vec3 seedHash = hash3(vec3(seed, seed * seed * 3.14159, seed * 1.17421));
+
+  vec3 gridBasePosition = floor(coords);
+  vec3 gridCoordOffset = fract(coords);
+  float maxCellSearch = ceil(maxOffset) + 1.0;
+
+  float closest = 1.0;
+  for (float y = -maxCellSearch; y <= maxCellSearch; y += 1.0) {
+    for (float x = -maxCellSearch; x <= maxCellSearch; x += 1.0) {
+      for (float z = -maxCellSearch; z <= maxCellSearch; z += 1.0) {
+        vec3 neighbourCellPosition = vec3(x, y, z);
+        vec3 cellWorldPosition = gridBasePosition + neighbourCellPosition;
+        cellWorldPosition.xy = mod(cellWorldPosition.xy, vec2(cellsMult));
+
+        vec3 cellOffset = hash3(cellWorldPosition + seedHash);
+        cellOffset *= maxOffset;
+
+        float distToNeighbour = length(
+            neighbourCellPosition + cellOffset - gridCoordOffset);
+        closest = min(closest, distToNeighbour);
+      }
+    }
+  }
+
+  return saturate(closest);
+}
+
+float gradientNoise(vec3 p, float tileLength) {
+  vec3 i = floor(p);
+  vec3 f = fract(p);
+	
+	vec3 u = smootherstep3(vec3(0.0), vec3(1.0), f);
+    
+  /*
+  * For 1D, the gradient of slope g at vertex u has the form h(x) = g * (x - u), where u 
+  * is an integer and g is in [-1, 1]. This is the equation for a line with slope g which 
+  * intersects the x-axis at u.
+  * For N dimensional noise, use dot product instead of multiplication, and do 
+  * component-wise interpolation (for 3D, trilinear)
+  */
+  return mix( mix( mix( dot( hash3_New(i + vec3(0.0,0.0,0.0), tileLength), f - vec3(0.0,0.0,0.0)), 
+                        dot( hash3_New(i + vec3(1.0,0.0,0.0), tileLength), f - vec3(1.0,0.0,0.0)), u.x),
+                   mix( dot( hash3_New(i + vec3(0.0,1.0,0.0), tileLength), f - vec3(0.0,1.0,0.0)), 
+                        dot( hash3_New(i + vec3(1.0,1.0,0.0), tileLength), f - vec3(1.0,1.0,0.0)), u.x), u.y),
+              mix( mix( dot( hash3_New(i + vec3(0.0,0.0,1.0), tileLength), f - vec3(0.0,0.0,1.0)), 
+                        dot( hash3_New(i + vec3(1.0,0.0,1.0), tileLength), f - vec3(1.0,0.0,1.0)), u.x),
+                   mix( dot( hash3_New(i + vec3(0.0,1.0,1.0), tileLength), f - vec3(0.0,1.0,1.0)), 
+                        dot( hash3_New(i + vec3(1.0,1.0,1.0), tileLength), f - vec3(1.0,1.0,1.0)), u.x), u.y), u.z );
+}
+
+// https://github.com/sebh/TileableVolumeNoise/blob/master/TileableVolumeNoise.cpp
+float tileableFBM(vec3 p, float tileLength) {
+  const float persistence = 0.5;
+  const float lacunarity = 2.0;
+  const int octaves = 8;
+
+  float amplitude = 0.5;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = gradientNoise(p, tileLength * lacunarity * 0.5) * 0.5 + 0.5;
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    p = p * lacunarity;
+  }
+
+  total /= normalization;
+  total = smootherstep(0.0, 1.0, total);
+
+  return total;
+}
+
+// https://github.com/sebh/TileableVolumeNoise/blob/master/main.cpp
+vec4 PerlinWorleyDetails(float CELL_RANGE, float numCells, float mult) {
+  float perlinWorley = 0.0;
+  {
+    float worley0 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 2.0 * mult, 1.0);
+    float worley1 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 8.0 * mult, 2.0);
+    float worley2 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 16.0 * mult, 3.0);
+    float worleyFBM = worley0 * 0.625 + worley1 * 0.25 + worley2 * 0.125;
+
+    float tileLength = 8.0;
+    float fbm0 = tileableFBM(vec3(vUvs * tileLength, zLevel) * mult, tileLength);
+
+    perlinWorley = remap(fbm0, 0.0, 1.0, worleyFBM, 1.0);
+  }
+
+  float worley0 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 1.0 * mult, 4.0);
+  float worley1 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 2.0 * mult, 5.0);
+  float worley2 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 4.0 * mult, 6.0);
+  float worley3 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 8.0 * mult, 7.0);
+  float worley4 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 16.0 * mult, 8.0);
+  float worley5 = 1.0 - voronoiSlow(CELL_RANGE, numCells * 32.0 * mult, 9.0);
+
+  float worleyFBM0 = worley1 * 0.625 + worley2 * 0.25 + worley3 * 0.125;
+  float worleyFBM1 = worley2 * 0.625 + worley3 * 0.25 + worley4 * 0.125;
+  float worleyFBM2 = worley3 * 0.625 + worley4 * 0.25 + worley5 * 0.125;
+
+  float lowFreqFBM = worleyFBM0 * 0.625 + worleyFBM1 * 0.25 + worleyFBM2 * 0.125;
+
+  float tileLength = 8.0;
+  float perlinWorleyDetail = remap(perlinWorley, lowFreqFBM, 1.0, 0.0, 1.0);
+
+  return vec4(perlinWorley, perlinWorleyDetail, 0.0, 0.0);
+}
+
+
+void main() {
+  const float CELL_RANGE = 1.0;
+
+  vec4 perlinWorleyDetail1 = PerlinWorleyDetails(CELL_RANGE, numCells, 1.0);
+
+  gl_FragColor = perlinWorleyDetail1;
+}
+

+ 34 - 0
shaders/header.glsl

@@ -0,0 +1,34 @@
+
+precision highp float;
+precision highp sampler2DArray;
+precision highp sampler3D;
+precision highp float;
+precision highp int;
+
+varying vec2 vUvs;
+uniform vec2 resolution;
+uniform float time;
+uniform int frame;
+uniform sampler2D blueNoise;
+
+uniform sampler3D perlinWorley;
+
+
+vec3 COLOUR_LIGHT_BLUE = vec3(0.42, 0.65, 0.85);
+vec3 COLOUR_LIGHT_GREEN = vec3(0.25, 1.0, 0.25);
+vec3 COLOUR_PALE_GREEN = vec3(0.42, 0.85, 0.65);
+vec3 COLOUR_LIGHT_PURPLE = vec3(0.85, 0.25, 0.85);
+vec3 COLOUR_BRIGHT_PINK = vec3(1.0, 0.5, 0.5);
+
+vec3 COLOUR_BRIGHT_RED = vec3(1.0, 0.1, 0.02);
+vec3 COLOUR_BRIGHT_BLUE = vec3(0.01, 0.2, 1.0);
+vec3 COLOUR_BRIGHT_GREEN = vec3(0.01, 1.0, 0.2);
+vec3 COLOUR_PALE_BLUE = vec3(0.42, 0.65, 0.85);
+vec3 COLOUR_LIGHT_YELLOW = vec3(1.0, 1.0, 0.25);
+
+
+const float TIME_OFFSET = 0.0;
+const float TIME_SPEED = 1.0;
+
+
+#define USE_OKLAB

+ 410 - 0
shaders/noise.glsl

@@ -0,0 +1,410 @@
+
+// The MIT License
+// Copyright © 2013 Inigo Quilez
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+// https://www.youtube.com/c/InigoQuilez
+// https://iquilezles.org/
+// https://www.shadertoy.com/view/lsf3WH
+
+
+float hash(vec2 p)  // replace this by something better
+{
+    p  = 50.0*fract( p*0.3183099 + vec2(0.71,0.113));
+    return -1.0+2.0*fract( p.x*p.y*(p.x+p.y) );
+}
+
+float hash(vec3 p)  // replace this by something better
+{
+    p  = 50.0*fract( p*0.3183099 + vec3(0.71, 0.113, 0.5231));
+    return -1.0+2.0*fract( p.x*p.y*p.z*(p.x+p.y+p.z) );
+}
+
+vec2 hash2(vec2 p)
+{
+ 	return fract(cos(p*mat2(57.3,37.36,83.17,-23.2))*28.9) * 2.0 - 1.0;   
+}
+
+vec2 hash2( ivec2 z )  // replace this anything that returns a random vector
+{
+    // 2D to 1D  (feel free to replace by some other)
+    int n = z.x+z.y*11111;
+
+    // Hugo Elias hash (feel free to replace by another one)
+    n = (n<<13)^n;
+    n = (n*(n*n*15731+789221)+1376312589)>>16;
+
+    // Perlin style vectors
+    n &= 7;
+    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
+    return ( n>=6 ) ? vec2(0.0,gr.x) : 
+           ( n>=4 ) ? vec2(gr.x,0.0) :
+                              gr;
+}
+
+vec3 hash3(vec2 p ) {
+	vec3 p3 = vec3( dot(p,vec2(127.1,311.7)),
+			  dot(p,vec2(269.5,183.3)),
+			  dot(p,vec2(113.5,271.9)));
+
+	return -1.0 + 2.0*fract(sin(p3)*43758.5453123);
+}
+
+vec3 hash3( vec3 p ) // replace this by something better
+{
+	p = vec3( dot(p,vec3(127.1,311.7, 74.7)),
+			  dot(p,vec3(269.5,183.3,246.1)),
+			  dot(p,vec3(113.5,271.9,124.6)));
+
+	return -1.0 + 2.0*fract(sin(p)*43758.5453123);
+}
+
+vec3 hash3( vec4 p ) // replace this by something better
+{
+	vec3 r = vec3(
+      dot(p, vec4(127.1, 311.7, 74.7, 93.124)),
+      dot(p, vec4(269.5, 183.3, 246.1, 55.432)),
+      dot(p, vec4(113.5, 271.9, 124.6, 6.823)));
+
+	return -1.0 + 2.0*fract(sin(r)*43758.5453123);
+}
+
+vec4 hash4( vec4 p ) // replace this by something better
+{
+	p = vec4( dot(p, vec4(127.1,311.7,74.7,93.124)),
+            dot(p, vec4(269.5,183.3,246.1,55.432)),
+            dot(p, vec4(113.5,271.9,124.6,6.823)),
+            dot(p, vec4(37.643,83.42,17.531,11.952)));
+
+	return -1.0 + 2.0*fract(sin(p)*43758.5453123);
+}
+
+uint murmurHash11(uint src) {
+    const uint M = 0x5bd1e995u;
+    uint h = 1190494759u;
+    src *= M; src ^= src>>24u; src *= M;
+    h *= M; h ^= src;
+    h ^= h>>13u; h *= M; h ^= h>>15u;
+    return h;
+}
+
+// 1 output, 1 input
+// float hash1(float src) {
+//     uint h = murmurHash11(floatBitsToUint(src));
+//     return (uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0) * 2.0 - 1.0;
+// }
+float hash1( float p ) // replace this by something better
+{
+	p = p * 321.17;
+
+	return -1.0 + 2.0*fract(sin(p)*43758.5453123);
+}
+
+// noise definition
+float noise( in vec4 p ) {
+  vec4 i = floor( p );
+  vec4 f = fract( p );
+
+	vec4 u = f * f * (3.0 - 2.0 * f);
+
+  float z1 = mix( mix( mix( dot( hash4( i + vec4(0.0,0.0,0.0,0.0) ), f - vec4(0.0,0.0,0.0,0.0) ), 
+                            dot( hash4( i + vec4(1.0,0.0,0.0,0.0) ), f - vec4(1.0,0.0,0.0,0.0) ), u.x),
+                       mix( dot( hash4( i + vec4(0.0,1.0,0.0,0.0) ), f - vec4(0.0,1.0,0.0,0.0) ), 
+                            dot( hash4( i + vec4(1.0,1.0,0.0,0.0) ), f - vec4(1.0,1.0,0.0,0.0) ), u.x), u.y),
+                  mix( mix( dot( hash4( i + vec4(0.0,0.0,1.0,0.0) ), f - vec4(0.0,0.0,1.0,0.0) ), 
+                            dot( hash4( i + vec4(1.0,0.0,1.0,0.0) ), f - vec4(1.0,0.0,1.0,0.0) ), u.x),
+                       mix( dot( hash4( i + vec4(0.0,1.0,1.0,0.0) ), f - vec4(0.0,1.0,1.0,0.0) ), 
+                            dot( hash4( i + vec4(1.0,1.0,1.0,0.0) ), f - vec4(1.0,1.0,1.0,0.0) ), u.x), u.y), u.z );
+  float z2 = mix( mix( mix( dot( hash4( i + vec4(0.0,0.0,0.0,1.0) ), f - vec4(0.0,0.0,0.0,1.0) ), 
+                            dot( hash4( i + vec4(1.0,0.0,0.0,1.0) ), f - vec4(1.0,0.0,0.0,1.0) ), u.x),
+                       mix( dot( hash4( i + vec4(0.0,1.0,0.0,1.0) ), f - vec4(0.0,1.0,0.0,1.0) ), 
+                            dot( hash4( i + vec4(1.0,1.0,0.0,1.0) ), f - vec4(1.0,1.0,0.0,1.0) ), u.x), u.y),
+                  mix( mix( dot( hash4( i + vec4(0.0,0.0,1.0,1.0) ), f - vec4(0.0,0.0,1.0,1.0) ), 
+                            dot( hash4( i + vec4(1.0,0.0,1.0,1.0) ), f - vec4(1.0,0.0,1.0,1.0) ), u.x),
+                       mix( dot( hash4( i + vec4(0.0,1.0,1.0,1.0) ), f - vec4(0.0,1.0,1.0,1.0) ), 
+                            dot( hash4( i + vec4(1.0,1.0,1.0,1.0) ), f - vec4(1.0,1.0,1.0,1.0) ), u.x), u.y), u.z );
+  return mix(z1, z2, u.w);
+}
+
+float noise( in vec3 p )
+{
+    vec3 i = floor( p );
+    vec3 f = fract( p );
+	
+	vec3 u = f*f*(3.0-2.0*f);
+
+    return mix( mix( mix( dot( hash3( i + vec3(0.0,0.0,0.0) ), f - vec3(0.0,0.0,0.0) ), 
+                          dot( hash3( i + vec3(1.0,0.0,0.0) ), f - vec3(1.0,0.0,0.0) ), u.x),
+                     mix( dot( hash3( i + vec3(0.0,1.0,0.0) ), f - vec3(0.0,1.0,0.0) ), 
+                          dot( hash3( i + vec3(1.0,1.0,0.0) ), f - vec3(1.0,1.0,0.0) ), u.x), u.y),
+                mix( mix( dot( hash3( i + vec3(0.0,0.0,1.0) ), f - vec3(0.0,0.0,1.0) ), 
+                          dot( hash3( i + vec3(1.0,0.0,1.0) ), f - vec3(1.0,0.0,1.0) ), u.x),
+                     mix( dot( hash3( i + vec3(0.0,1.0,1.0) ), f - vec3(0.0,1.0,1.0) ), 
+                          dot( hash3( i + vec3(1.0,1.0,1.0) ), f - vec3(1.0,1.0,1.0) ), u.x), u.y), u.z );
+}
+
+float noise( in vec2 p )
+{
+    vec2 i = vec2(floor( p ));
+     vec2 f =       fract( p );
+	
+	vec2 u = f*f*(3.0-2.0*f); // feel free to replace by a quintic smoothstep instead
+
+    return mix( mix( dot( hash2( i+vec2(0,0) ), f-vec2(0.0,0.0) ), 
+                     dot( hash2( i+vec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
+                mix( dot( hash2( i+vec2(0,1) ), f-vec2(0.0,1.0) ), 
+                     dot( hash2( i+vec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
+}
+
+float noise( float p )
+{
+  float i = floor( p );
+  float f = fract( p );
+	
+	float u = f*f*(3.0-2.0*f); // feel free to replace by a quintic smoothstep instead
+
+  return mix( hash1( i + 0.0 ) * (f - 0.0), 
+              hash1( i + 1.0 ) * (f - 1.0), u);
+}
+
+vec2 noise2(vec2 p) {
+  return vec2(
+      noise(p),
+      noise(p + vec2(243.02935, 743.87439))
+  );
+}
+
+vec3 noise3(vec2 p) {
+  return vec3(
+      noise(p),
+      noise(p + vec2(243.02935, 743.87439)),
+      noise(p + vec2(731.8735, 912.4724))
+  );
+}
+
+vec3 noise3(vec3 p) {
+  return vec3(
+      noise(p),
+      noise(p + vec3(243.02935, 743.87439, -17.5325)),
+      noise(p + vec3(731.8735, 912.4724, 1231.43297))
+  );
+}
+
+vec4 noise4(vec2 p) {
+  return vec4(
+      noise2(p),
+      noise2(p.xy + vec2(314.421, -432.32))
+  );
+}
+
+// return value noise (in x) and its derivatives (in yzw)
+vec4 noiseD( in vec3 x )
+{
+  // grid
+  vec3 i = floor(x);
+  vec3 w = fract(x);
+
+  // quintic interpolant
+  vec3 u = w*w*w*(w*(w*6.0-15.0)+10.0);
+  vec3 du = 30.0*w*w*(w*(w-2.0)+1.0);
+
+  // gradients
+  vec3 ga = hash3( i+vec3(0.0,0.0,0.0) );
+  vec3 gb = hash3( i+vec3(1.0,0.0,0.0) );
+  vec3 gc = hash3( i+vec3(0.0,1.0,0.0) );
+  vec3 gd = hash3( i+vec3(1.0,1.0,0.0) );
+  vec3 ge = hash3( i+vec3(0.0,0.0,1.0) );
+  vec3 gf = hash3( i+vec3(1.0,0.0,1.0) );
+  vec3 gg = hash3( i+vec3(0.0,1.0,1.0) );
+  vec3 gh = hash3( i+vec3(1.0,1.0,1.0) );
+
+  // projections
+  float va = dot( ga, w-vec3(0.0,0.0,0.0) );
+  float vb = dot( gb, w-vec3(1.0,0.0,0.0) );
+  float vc = dot( gc, w-vec3(0.0,1.0,0.0) );
+  float vd = dot( gd, w-vec3(1.0,1.0,0.0) );
+  float ve = dot( ge, w-vec3(0.0,0.0,1.0) );
+  float vf = dot( gf, w-vec3(1.0,0.0,1.0) );
+  float vg = dot( gg, w-vec3(0.0,1.0,1.0) );
+  float vh = dot( gh, w-vec3(1.0,1.0,1.0) );
+
+  // interpolations
+  return vec4(
+      va + u.x*(vb-va) + u.y*(vc-va) + u.z*(ve-va) + u.x*u.y*(va-vb-vc+vd) + u.y*u.z*(va-vc-ve+vg) +
+           u.z*u.x*(va-vb-ve+vf) + (-va+vb+vc-vd+ve-vf-vg+vh)*u.x*u.y*u.z,    // value
+      ga + u.x*(gb-ga) + u.y*(gc-ga) + u.z*(ge-ga) + u.x*u.y*(ga-gb-gc+gd) + u.y*u.z*(ga-gc-ge+gg) +
+           u.z*u.x*(ga-gb-ge+gf) + (-ga+gb+gc-gd+ge-gf-gg+gh)*u.x*u.y*u.z +   // derivatives
+           du * (vec3(vb,vc,ve) - va + u.yzx*vec3(va-vb-vc+vd,va-vc-ve+vg,va-vb-ve+vf) +
+              u.zxy*vec3(va-vb-ve+vf,va-vb-vc+vd,va-vc-ve+vg) + u.yzx*u.zxy*(-va+vb+vc-vd+ve-vf-vg+vh) ));
+}
+
+const mat2 FBM_M = mat2( 0.80,  0.60, -0.60,  0.80 );
+const mat3 FBM_M3 = mat3( 0.00,  0.80,  0.60,
+                    -0.80,  0.36, -0.48,
+                    -0.60, -0.48,  0.64 );
+
+float fbm(vec3 p, int octaves, float persistence, float lacunarity, float exponentiation) {
+  float amplitude = 1.0;
+  float frequency = 1.0;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise(p * frequency);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    frequency *= lacunarity;
+  }
+
+  total /= normalization;
+  total = total * 0.5 + 0.5;
+  total = pow(total, exponentiation);
+  // total = 1.0 - abs(total);
+
+  return total;
+}
+
+// noiseFBM function
+float noiseFBM(vec4 p, int octaves, float persistence, float lacunarity) {
+  float amplitude = 0.5;
+  float frequency = 1.0;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise(p * frequency);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    frequency *= lacunarity;
+  }
+
+  return total;
+}
+
+float noiseFBM(vec3 p, int octaves, float persistence, float lacunarity) {
+  float amplitude = 0.5;
+  float frequency = 1.0;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise(p * frequency);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    frequency *= lacunarity;
+  }
+
+  return total;
+}
+
+float noiseFBM(vec2 p, int octaves, float persistence, float lacunarity) {
+  float amplitude = 0.5;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise(p);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    p = FBM_M * p * lacunarity;
+  }
+
+  return total;
+}
+
+float noiseFBM(float p, int octaves, float persistence, float lacunarity) {
+  float amplitude = 0.5;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise(p);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    p = p * lacunarity;
+  }
+
+  total /= normalization;
+
+  return total;
+}
+
+// noiseFBM3 definition
+vec3 noiseFBM3(vec3 p, int octaves, float persistence, float lacunarity) {
+  return vec3(
+      noiseFBM(p, octaves, persistence, lacunarity),
+      noiseFBM(p + 13.2317, octaves, persistence, lacunarity),
+      noiseFBM(p + -34.934, octaves, persistence, lacunarity)
+  );
+}
+
+vec3 noiseFBM3(vec4 p, int octaves, float persistence, float lacunarity) {
+  return vec3(
+      noiseFBM(p, octaves, persistence, lacunarity),
+      noiseFBM(p + 13.2317, octaves, persistence, lacunarity),
+      noiseFBM(p + -34.934, octaves, persistence, lacunarity)
+  );
+}
+
+vec2 noiseFBM2(vec2 p, int octaves, float persistence, float lacunarity) {
+  return vec2(
+      noiseFBM(p, octaves, persistence, lacunarity),
+      noiseFBM(p + 13.2317, octaves, persistence, lacunarity)
+  );
+}
+
+vec2 noiseFBM2(vec3 p, int octaves, float persistence, float lacunarity) {
+  return vec2(
+      noiseFBM(p, octaves, persistence, lacunarity),
+      noiseFBM(p + vec3(432.532, 5326.3245, -95.9043), octaves, persistence, lacunarity)
+  );
+}
+
+float noiseWarp(vec3 coords) {
+  vec3 offset = vec3(
+    noiseFBM(coords, 4, 0.5, 2.0),
+    noiseFBM(coords + vec3(43.235, 23.112, 0.0), 4, 0.5, 2.0), 0.0);
+  float noiseSample = noiseFBM(coords + offset, 1, 0.5, 2.0);
+
+  vec3 offset2 = vec3(
+    noiseFBM(coords + 4.0 * offset + vec3(5.325, 1.421, 3.235), 4, 0.5, 2.0),
+    noiseFBM(coords + 4.0 * offset + vec3(4.32, 0.532, 6.324), 4, 0.5, 2.0), 0.0);
+  noiseSample = noiseFBM(coords + 4.0 * offset2, 1, 0.5, 2.0);
+
+  return noiseSample;
+}
+
+float noiseWarp(vec2 coords) {
+  vec2 offset = noiseFBM2(coords, 4, 0.5, 2.0);
+  float noiseSample = noiseFBM(coords + offset, 1, 0.5, 2.0);
+
+  vec2 offset2 = noiseFBM2(coords + 4.0 * offset + vec2(5.325, 1.421), 4, 0.5, 2.0);
+  noiseSample = noiseFBM(coords + 4.0 * offset2, 1, 0.5, 2.0);
+
+  return noiseSample;
+}
+
+float voronoi(vec2 coords, float maxOffset) {
+  vec2 gridBasePosition = floor(coords.xy);
+  vec2 gridCoordOffset = fract(coords.xy);
+
+  float closest = 1.0;
+  for (float y = -2.0; y <= 2.0; y += 1.0) {
+    for (float x = -2.0; x <= 2.0; x += 1.0) {
+      vec2 neighbourCellPosition = vec2(x, y);
+      vec2 cellWorldPosition = gridBasePosition + neighbourCellPosition;
+      vec2 cellOffset = hash2(cellWorldPosition);
+      cellOffset *= maxOffset;
+
+      float distToNeighbour = length(
+          neighbourCellPosition + cellOffset - gridCoordOffset);
+      closest = min(closest, distToNeighbour);
+    }
+  }
+
+  return closest;
+}

+ 58 - 0
shaders/oklab.glsl

@@ -0,0 +1,58 @@
+
+// https://bottosson.github.io/posts/oklab/
+
+
+float cbrtf(float x) {
+  return pow(x, 1.0 / 3.0);
+}
+
+vec3 rgbToOklab(vec3 c) 
+{
+  float l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
+	float m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
+	float s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;
+
+    float l_ = cbrtf(l);
+    float m_ = cbrtf(m);
+    float s_ = cbrtf(s);
+
+    return vec3(
+        0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_,
+        1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_,
+        0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_
+    );
+}
+
+vec3 oklabToRGB(vec3 c) 
+{
+    float l_ = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;
+    float m_ = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;
+    float s_ = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;
+
+    float l = l_*l_*l_;
+    float m = m_*m_*m_;
+    float s = s_*s_*s_;
+
+    return vec3(
+        +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
+        -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
+        -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
+    );
+}
+
+
+#ifndef USE_OKLAB
+#define col3 vec3
+#else
+vec3 col3(float r, float g, float b) {
+  return rgbToOklab(vec3(r, g, b));
+}
+
+vec3 col3(vec3 v) {
+  return rgbToOklab(v);
+}
+
+vec3 col3(float v) {
+  return rgbToOklab(vec3(v));
+}
+#endif

+ 136 - 0
shaders/sdf-generator-shader.glsl

@@ -0,0 +1,136 @@
+uniform float zLevel;
+uniform float numCells;
+
+
+float GenerateGridClouds(vec3 coords, float seed, float CELL_WIDTH) {
+  vec3 cellCoords = vec3((fract(coords.xy / CELL_WIDTH) - 0.5) * CELL_WIDTH, coords.z);
+  vec2 cellID = floor(coords.xy / CELL_WIDTH) + hash2(vec2(seed));
+
+  float radius = remap(hash1(seed), -1.0, 1.0, 25.0, 45.0);
+
+  vec3 cellHashValue = hash3(vec3(cellID, seed)) * (CELL_WIDTH * 0.5 - radius);
+  vec3 cloudPosition = vec3(0.0);
+
+  cloudPosition.xy += cellHashValue.xy;
+  // cloudPosition.z += cellHashValue.z * 0.1;
+
+  return sdfSphere(cellCoords - cloudPosition, radius);
+}
+
+float GenerateCloudCluster(vec3 coords, float seed, float CELL_WIDTH) {
+  if (hash1(seed * 3.124513 + 0.001) < 0.5) {
+    return 100000.0;
+  }
+
+  vec3 cellCoords = vec3((fract(coords.xy / CELL_WIDTH) - 0.5) * CELL_WIDTH, coords.z);
+  vec2 cellID = floor(coords.xy / CELL_WIDTH) + hash2(vec2(seed));
+
+  float radius = 40.0;
+
+  vec3 cellHashValue = hash3(vec3(cellID, seed));
+  vec3 cloudPosition = cellHashValue * vec3(1.0, 1.0, 0.0) * (CELL_WIDTH * 0.25 - radius);
+  cloudPosition.z = -radius * 0.5;
+
+  float sdfValue = sdfSphere(cellCoords - cloudPosition, radius);
+
+  for (float i = 0.0; i < 32.0; ++i) {
+    float curSeed = seed + i * 0.0001;
+
+    radius = remap(hash1(curSeed), -1.0, 1.0, 30.0, 40.0);
+    cellHashValue = hash3(vec3(cellID, curSeed)) * (CELL_WIDTH * 0.125 - radius);
+    vec3 offset = cellHashValue * vec3(1.0, 1.0, 0.0);
+    offset.z = -radius * 0.5;
+
+    sdfValue = min(sdfValue, sdfSphere(cellCoords - (cloudPosition + offset), radius));
+  }
+
+  for (float i = 0.0; i < 32.0; ++i) {
+    float curSeed = seed + i * 0.0001 + 6429.264358;
+
+    radius = remap(hash1(curSeed), -1.0, 1.0, 10.0, 20.0);
+    cellHashValue = hash3(vec3(cellID, curSeed)) * (CELL_WIDTH * 0.175 - radius);
+    vec3 offset = cellHashValue * vec3(1.0, 1.0, 0.0);
+    offset.z = -radius * 0.5;
+
+    sdfValue = min(sdfValue, sdfSphere(cellCoords - (cloudPosition + offset), radius));
+  }
+  return sdfValue;
+}
+
+float voronoiSlow(vec3 coords, float maxOffset, float cellsMult, float seed, float fixedZ, float cutoff) {
+  vec2 seedHash = hash2(vec2(seed, seed * seed * 3.14159));
+
+  vec2 gridBasePosition = floor(coords.xy);
+  vec2 gridCoordOffset = fract(coords.xy);
+  float maxCellSearch = ceil(maxOffset) + 1.0;
+
+  vec3 gridCoordOffsetZ = fract(coords);
+
+  float closest = 1.0;
+  for (float y = -maxCellSearch; y <= maxCellSearch; y += 1.0) {
+    for (float x = -maxCellSearch; x <= maxCellSearch; x += 1.0) {
+      vec2 neighbourCellPosition = vec2(x, y);
+      vec2 cellWorldPosition = gridBasePosition + neighbourCellPosition;
+      cellWorldPosition.xy = mod(cellWorldPosition.xy, vec2(cellsMult));
+
+      if (hash(cellWorldPosition + seedHash) < cutoff) {
+        continue;
+      }
+
+      vec2 cellOffset = hash2(cellWorldPosition + seedHash);
+      cellOffset *= maxOffset;
+
+      // Figure out cell position of fixedZ
+      vec3 neighbourCellPositionZ = vec3(neighbourCellPosition, floor(fixedZ - coords.z)) + vec3(cellOffset, 0.0);
+
+      float distToNeighbour = length(neighbourCellPositionZ - gridCoordOffsetZ);
+      closest = min(closest, distToNeighbour);
+    }
+  }
+
+  return saturate(closest);
+}
+
+float getCloudMap(vec3 pos) {
+  float mult = 16.0;
+  float v1 = 0.0;
+
+  mult = 16.0;
+  // v1 = max(v1, 1.0 - voronoiSlow(pos * mult, 2.0, 8.0, 1.0, 0.5 * mult, 0.85));
+
+  mult = 8.0;
+  v1 = max(v1, 1.0 - voronoiSlow(pos * mult, 4.0, 8.0, 2.0, 0.5 * mult, 0.85));
+
+  // mult = 4.0;
+  // v1 = max(v1, 1.0 - voronoiSlow(pos * mult, 2.0, 4.0, 3.0, 0.5 * mult, 0.5));
+
+  // Layer 2
+  // mult = 16.0;
+  // v1 = max(v1, 1.0 - voronoiSlow(pos * mult, 4.0, 8.0, -1.0, 0.75 * mult, 0.75));
+
+  mult = 8.0;
+  for (float i = 0.0; i < 10.0; i++) {
+    v1 = max(v1, 1.0 - voronoiSlow(pos * mult, 4.0, 8.0, i + 1523.6234, 0.5 * mult, 0.75));
+    v1 = max(v1, 1.0 - voronoiSlow(pos * mult, 2.0, 8.0, i + 2355.6325, 0.5 * mult, 0.65));
+  }
+
+  return v1;
+}
+
+void main() {
+  vec3 pos = vec3(vUvs, remap(zLevel, 0.0, 31.0, 0.0, 1.0));
+  float res = getCloudMap(pos);
+
+  float v1 = smoothstep(0.5, 0.3, abs(vUvs.x - 0.5));
+  float v2 = smoothstep(0.5, 0.3, abs(vUvs.y - 0.5));
+  float v3 = smoothstep(16.0, 12.0, abs(zLevel - 16.0));
+  float v = v1 * v2 * v3;
+  // v = pow(v, 2.0);
+  // res *= v;
+
+// float mult = 7.0;
+// res =  1.0 - voronoiSlow(vec3(pos.xy, 0.0) * mult, 0.5, mult, 1.0, 0.0, -1.0);
+
+  gl_FragColor = vec4(res);
+}
+

+ 0 - 0
shaders/ui2d.glsl


+ 7 - 0
shaders/vertex-shader.glsl

@@ -0,0 +1,7 @@
+
+varying vec2 vUvs;
+
+void main() {	
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+  vUvs = uv;
+}

二進制
textures/HDR_L_0.png