Simon 1 рік тому
батько
коміт
938246e4ff

+ 9 - 0
base.css

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

+ 575 - 0
demo.js

@@ -0,0 +1,575 @@
+import * as THREE from 'three';
+import { LazyBrush } from 'lazy-brush';
+import { Pane } from 'tweakpane';
+import Stats from 'three/examples/jsm/libs/stats.module'
+
+const cascade0_Dims = 2.0;
+const cascade0_Range = 1.0;
+const aspect = 16.0 / 9.0;
+const SCENE_RES = new THREE.Vector2(1920, 1080);
+const CASCADE_RES = new THREE.Vector2(1024 * aspect, 1024);
+
+const LAZY_RADIUS = 60;
+
+class SimonDevGLSLCourse {
+  constructor() {
+  }
+
+  async initialize() {
+    this.threejs_ = new THREE.WebGLRenderer();
+    document.body.appendChild(this.threejs_.domElement);
+
+    this.params_ = {
+      brush: {
+        radius: {
+          radius: 20.0,
+          min: 10.0,
+          max: 400.0,
+        },
+        colour: {
+          colour: { r: 1.0, g: 1.0, b: 1.0 },
+        },
+        friction: {
+          friction: 5,
+          min: 1,
+          max: 100,
+          step: 1.
+        },
+      }
+    }
+
+    const pane = new Pane({
+      title: 'Brush',
+    });
+    pane.addBinding(this.params_.brush.radius, 'radius', {
+      min: this.params_.brush.radius.min,
+      max: this.params_.brush.radius.max,
+      step: 1,
+    });
+    pane.addBinding(this.params_.brush.friction, 'friction', {
+      min: this.params_.brush.friction.min,
+      max: this.params_.brush.friction.max,
+      step: this.params_.brush.friction.step,
+    });
+    pane.addBinding(this.params_.brush.colour, 'colour', {
+      color: {type: 'float'},
+    });
+
+    this.stats_ = Stats()
+    // document.body.appendChild(this.stats_.dom);
+
+    window.addEventListener('resize', () => {
+      this.onWindowResize_();
+    }, false);
+
+    this.camera_ = new THREE.OrthographicCamera(0, 1, 1, 0, 0.1, 1000);
+    this.camera_.position.set(0, 0, 1);
+
+    this.materials_ = [];
+    this.targets_ = [];
+
+    this.setupBrush_();
+
+    await this.setupProject_();
+
+    this.previousRAF_ = null;
+    this.onWindowResize_();
+    this.raf_();
+  }
+
+  setupBrush_() {
+    this.lazyBrush_ = new LazyBrush({
+      enabled: true,
+      radius: LAZY_RADIUS
+    });
+
+    this.brushCoords_ = {
+      x: -1,
+      y: -1,
+      current: null,
+      previous: null,
+      touching: false,
+      points: [],
+    };
+    this.threejs_.domElement.addEventListener('mousemove', (e) => {
+      const x = (e.clientX / window.innerWidth) * SCENE_RES.x;
+      const y = (e.clientY / window.innerHeight) * SCENE_RES.y;
+
+      this.brushCoords_.x = x;
+      this.brushCoords_.y = y;
+    });
+
+    this.threejs_.domElement.addEventListener('touchstart', (e) => {
+      this.brushCoords_.touching = true;
+    });
+
+    this.threejs_.domElement.addEventListener('mousedown', (e) => {
+      this.brushCoords_.touching = true;
+    });
+
+    this.threejs_.domElement.addEventListener('mouseup', (e) => {
+      this.brushCoords_.touching = false;
+      this.brushCoords_.points = [];
+      this.brushCoords_.current = null;
+      this.brushCoords_.previous = null;
+    });
+
+    this.threejs_.domElement.addEventListener('touchend', (e) => {
+      this.brushCoords_.touching = false;
+      this.brushCoords_.points = [];
+      this.brushCoords_.current = null;
+      this.brushCoords_.previous = null;
+    });
+  }
+
+  async setupProject_() {
+    const common = await fetch('./shaders/common.glsl');
+    const noise = await fetch('./shaders/noise.glsl');
+    const oklab = await fetch('./shaders/oklab.glsl');
+    const header = await fetch('./shaders/header.glsl');
+    const cascades = await fetch('./shaders/cascades.glsl');
+
+    const vshScene = await fetch('./shaders/scene-vertex-shader.glsl');
+    const fshScene = await fetch('./shaders/scene-fragment-shader.glsl');
+    const vshCopy = await fetch('./shaders/copy-vertex-shader.glsl');
+    const fshCopy = await fetch('./shaders/copy-fragment-shader.glsl');
+    const fshCopySDF = await fetch('./shaders/copy-sdf-fragment-shader.glsl');
+    const vshCascade = await fetch('./shaders/compute-cascade-vertex-shader.glsl');
+    const fshCascade = await fetch('./shaders/compute-cascade-fragment-shader.glsl');
+    const vshMerge = await fetch('./shaders/merge-cascades-vertex-shader.glsl');
+    const fshMerge = await fetch('./shaders/merge-cascades-fragment-shader.glsl');
+    const vshRadianceField = await fetch('./shaders/radiance-field-vertex-shader.glsl');
+    const fshRadianceField = await fetch('./shaders/radiance-field-fragment-shader.glsl');
+    const fshFinalCompose = await fetch('./shaders/final-compose-fragment-shader.glsl');
+
+    const commonText = await common.text() + '\n';
+    const noiseText = await noise.text() + '\n';
+    const oklabText = await oklab.text() + '\n';
+    const headerText = await header.text() + '\n';
+    const cascadesText = await cascades.text() + '\n';
+
+    const libsText = headerText + oklabText + commonText + noiseText;
+    const vshSceneText = libsText + await vshScene.text();
+    const fshSceneText = libsText + await fshScene.text();
+    const vshCopyText = libsText + await vshCopy.text();
+    const fshCopyText = libsText + await fshCopy.text();
+    const fshCopySDFText = libsText + await fshCopySDF.text();
+    const vshCascadeText = libsText + await vshCascade.text();
+    const fshCascadeText = libsText + cascadesText + await fshCascade.text();
+    const vshMergeText = libsText + await vshMerge.text();
+    const fshMergeText = libsText + cascadesText + await fshMerge.text();
+    const vshRadianceFieldText = libsText + cascadesText + await vshRadianceField.text();
+    const fshRadianceFieldText = libsText + cascadesText + await fshRadianceField.text();
+    const fshFinalComposeText = libsText + cascadesText + await fshFinalCompose.text();
+
+
+    // First pass
+    this.scene_ = new THREE.Scene();
+
+    this.sceneMaterial_ = new THREE.ShaderMaterial({
+        uniforms: {
+          brushPos: { value: new THREE.Vector2(0, 0) },
+          brushRadius: { value: 0 },
+          brushColour: { value: new THREE.Vector3(1, 1, 1) },
+          sdfTexture: { value: null },
+          time: { value: 0 },
+          resolution: { value: new THREE.Vector2(1, 1) },
+        },
+        vertexShader: vshSceneText,
+        fragmentShader: fshSceneText,
+        side: THREE.FrontSide
+      });
+
+    const screenQuad = new THREE.PlaneGeometry(1, 1);
+    const sceneQuad = new THREE.Mesh(screenQuad, this.sceneMaterial_);
+    sceneQuad.position.set(0.5, 0.5, 0);
+    this.scene_.add(sceneQuad);
+    this.materials_.push(this.sceneMaterial_);
+
+    const nearestOpts = {
+      minFilter: THREE.NearestFilter,
+      magFilter: THREE.NearestFilter,
+      format: THREE.RGBAFormat,
+      type: THREE.HalfFloatType,
+    };
+
+    const sceneOpts = {
+      minFilter: THREE.LinearMipMapLinearFilter,
+      magFilter: THREE.LinearFilter,
+      format: THREE.RGBAFormat,
+      type: THREE.HalfFloatType,
+      generateMipmaps: true,
+    };
+
+    const radianceOpts = {
+      minFilter: THREE.LinearFilter,
+      magFilter: THREE.LinearFilter,
+      format: THREE.RGBAFormat,
+      type: THREE.HalfFloatType,
+    };
+
+    this.sdfTargets_ = [
+      new THREE.WebGLRenderTarget(SCENE_RES.x, SCENE_RES.y, nearestOpts),
+      new THREE.WebGLRenderTarget(SCENE_RES.x, SCENE_RES.y, nearestOpts)
+    ];
+    this.sdfFinalTarget_ = new THREE.WebGLRenderTarget(SCENE_RES.x, SCENE_RES.y, nearestOpts);
+    this.sdfIndex_ = 0;
+
+    this.threejs_.setRenderTarget(this.sdfTargets_[0]);
+    this.threejs_.setClearColor(0x000000, 1000);
+    this.threejs_.clear();
+    this.threejs_.setRenderTarget(this.sdfTargets_[1]);
+    this.threejs_.clear();
+    this.threejs_.setClearColor(0x000000, 0);
+
+
+    // Compute cascades
+    const factor = 4.0;
+    const diagonalLength = (SCENE_RES.x ** 2 + SCENE_RES.y ** 2) ** 0.5;
+    let numCascadeLevels = Math.ceil(Math.log(diagonalLength / cascade0_Range) / Math.log(factor)) - 1;
+    console.log('numCascadeLevels: ' + numCascadeLevels);
+    console.log('diagonalLength: ' + diagonalLength);
+
+    for (let i = 0; true; ++i) {
+      const cascadeLevel = i;
+      const cascadeN_Start_Pixels = (cascade0_Range * (1.0 - (factor ** cascadeLevel))) / (1.0 - factor);
+      const cascadeN_End_Pixels = (cascade0_Range * (1.0 - (factor ** (cascadeLevel + 1)))) / (1.0 - factor);
+      if (cascadeN_End_Pixels > diagonalLength) {
+        console.log('ACTUAL FUCKING LEVEL: ', i);
+        numCascadeLevels = i + 1;
+        break;
+      }
+    }
+
+    this.cascadeRealTargets_ = [];
+    for (let i = 0; i < numCascadeLevels; i++) {
+      this.cascadeRealTargets_.push(new THREE.WebGLRenderTarget(CASCADE_RES.x, CASCADE_RES.y, nearestOpts));
+    }
+
+    this.cascadeMaterial_ = new THREE.ShaderMaterial({
+      uniforms: {
+        time: { value: 0 },
+        resolution: { value: new THREE.Vector2(1, 1) },
+        sceneResolution: { value: new THREE.Vector2(SCENE_RES.x, SCENE_RES.y) },
+        sceneTexture: { value: null },
+        sdfTexture: { value: null },
+        cascadeLevel: { value: 0.0 },
+        cascade0_Range: { value: cascade0_Range },
+        cascade0_Dims: { value: cascade0_Dims },
+        cascadeResolution: { value: new THREE.Vector2(CASCADE_RES.x, CASCADE_RES.y) },
+      },
+      vertexShader: vshCascadeText,
+      fragmentShader: fshCascadeText,
+    });
+
+    const cascadeQuad = new THREE.Mesh(screenQuad, this.cascadeMaterial_);
+    cascadeQuad.position.set(0.5, 0.5, 0);
+    this.cascadeQuad_ = cascadeQuad;
+
+    this.cascadeScene_ = new THREE.Scene();
+    this.cascadeScene_.add(cascadeQuad);
+
+    for (let cascadeLevel = 0; cascadeLevel < numCascadeLevels; cascadeLevel++) {
+      const cascadeN_Dims = cascade0_Dims * (2.0 ** cascadeLevel);
+      // const cascadeN_Start_Pixels = cascade0_Range * (factor ** cascadeLevel) - 1;
+      // const cascadeN_End_Pixels = cascade0_Range * (factor ** (cascadeLevel + 1)) - 1;
+      // interval0 * (1.0 - pow(4.0, factor))) / (1.0 - 4.0)
+      const cascadeN_Start_Pixels = (cascade0_Range * (1.0 - (factor ** cascadeLevel))) / (1.0 - factor);
+      const cascadeN_End_Pixels = (cascade0_Range * (1.0 - (factor ** (cascadeLevel + 1)))) / (1.0 - factor);
+  
+      console.log('cascade level: ' + cascadeLevel);
+      console.log('start: ' + cascadeN_Start_Pixels);
+      console.log('end  : ' + cascadeN_End_Pixels);
+      console.log('dims : ' + cascadeN_Dims);
+    }
+
+    this.materials_.push(this.cascadeMaterial_);
+
+    // Merge cascades
+    this.cascadeMergeMaterial_ = new THREE.ShaderMaterial({
+      uniforms: {
+        time: { value: 0 },
+        resolution: { value: new THREE.Vector2(1, 1) },
+        sdfTexture: { value: null },
+        sceneResolution: { value: new THREE.Vector2(SCENE_RES.x, SCENE_RES.y) },
+        cascadeRealTexture: { value: null },
+        nextCascadeMergedTexture: { value: null },
+        prevCascadeTexture: { value: null },
+        currentCascadeLevel: { value: 0 },
+        numCascadeLevels: { value: numCascadeLevels },
+        cascade0_Range: { value: cascade0_Range },
+        cascade0_Dims: { value: cascade0_Dims },
+        cascadeResolution: { value: new THREE.Vector2(CASCADE_RES.x, CASCADE_RES.y) },
+      },
+      vertexShader: vshMergeText,
+      fragmentShader: fshMergeText,
+    });
+
+    const cascade0_ProbesX = Math.floor(CASCADE_RES.x);
+    const cascade0_ProbesY = CASCADE_RES.y;
+    this.cascadeMergeTargets_ = [];
+    for (let i = 0; i < numCascadeLevels; i++) {
+      this.cascadeMergeTargets_.push(new THREE.WebGLRenderTarget(CASCADE_RES.x, CASCADE_RES.y, nearestOpts));
+    }
+
+    this.cascadeMergeMesh_ = new THREE.Mesh(screenQuad, this.cascadeMergeMaterial_);
+    this.cascadeMergeMesh_.position.set(0.5, 0.5, 0);
+    this.cascadeMergeScene_ = new THREE.Scene();
+    this.cascadeMergeScene_.add(this.cascadeMergeMesh_);
+    this.materials_.push(this.cascadeMergeMaterial_);
+
+    console.log('cascade0_ProbesX: ' + cascade0_ProbesX);
+    console.log('cascade0_ProbesY: ' + cascade0_ProbesY);
+    
+    // Final pass to create radiance field
+    this.radianceFieldMat_ = new THREE.ShaderMaterial({
+      uniforms: {
+        resolution: { value: new THREE.Vector2(1, 1) },
+        time: { value: 0 },
+        cascade0_Dims: { value: cascade0_Dims },
+        cascadeResolution: { value: new THREE.Vector2(CASCADE_RES.x, CASCADE_RES.y) },
+        mergedCascade0Texture: { value: this.cascadeMergeTargets_[0].texture },
+        sceneResolution: { value: new THREE.Vector2(SCENE_RES.x, SCENE_RES.y) },
+      },
+      vertexShader: vshRadianceFieldText,
+      fragmentShader: fshRadianceFieldText,
+    });
+
+
+    this.radianceFieldTarget_ = new THREE.WebGLRenderTarget(
+        CASCADE_RES.x / cascade0_Dims,
+        CASCADE_RES.y / cascade0_Dims,
+        radianceOpts);
+    this.radianceFieldMesh_ = new THREE.Mesh(screenQuad, this.radianceFieldMat_);
+    this.radianceFieldMesh_.position.set(0.5, 0.5, 0);
+    this.radianceFieldScene_ = new THREE.Scene();
+    this.radianceFieldScene_.add(this.radianceFieldMesh_);
+    this.materials_.push(this.radianceFieldMat_);
+
+    // Final compose pass
+    this.finalComposeMat_ = new THREE.ShaderMaterial({
+      uniforms: {
+        radianceTexture: { value: null },
+        sceneTexture: { value: null },
+        sdfTexture: { value: this.sdfTargets_[0].texture },
+        resolution: { value: new THREE.Vector2(1, 1) },
+        time: { value: 0 },
+      },
+      vertexShader: vshCopyText,
+      fragmentShader: fshFinalComposeText,
+      side: THREE.FrontSide
+    });
+
+    this.finalComposeMesh_ = new THREE.Mesh(screenQuad, this.finalComposeMat_);
+    this.finalComposeMesh_.position.set(0.5, 0.5, 0);
+    this.finalComposeScene_ = new THREE.Scene();
+    this.finalComposeScene_.add(this.finalComposeMesh_);
+    this.materials_.push(this.finalComposeMat_);
+
+    // Copy pass
+    this.copyMat_ = new THREE.ShaderMaterial({
+      uniforms: {
+        diffuse: { value: null },
+        resolution: { value: new THREE.Vector2(1, 1) },
+        time: { value: 0 },
+      },
+      vertexShader: vshCopyText,
+      fragmentShader: fshCopyText,
+      side: THREE.FrontSide
+    });
+
+    this.copyMesh_ = new THREE.Mesh(screenQuad, this.copyMat_);
+    this.copyMesh_.position.set(0.5, 0.5, 0);
+    this.copyScene_ = new THREE.Scene();
+    this.copyScene_.add(this.copyMesh_);
+    this.materials_.push(this.copyMat_);
+
+    // Copy SDF pass
+    this.copySDFMat_ = new THREE.ShaderMaterial({
+      uniforms: {
+        brushPos1: { value: new THREE.Vector2(0, 0) },
+        brushPos2: { value: new THREE.Vector2(0, 0) },
+        brushRadius: { value: 0 },
+        brushColour: { value: new THREE.Vector3(1, 1, 1) },
+        sdfSource: { value: null },
+        resolution: { value: new THREE.Vector2(1, 1) },
+        time: { value: 0 },
+      },
+      vertexShader: vshCopyText,
+      fragmentShader: fshCopySDFText,
+      side: THREE.FrontSide
+    });
+
+    this.copySDFMesh_ = new THREE.Mesh(screenQuad, this.copySDFMat_);
+    this.copySDFMesh_.position.set(0.5, 0.5, 0);
+    this.copySDFScene_ = new THREE.Scene();
+    this.copySDFScene_.add(this.copySDFMesh_);
+    this.materials_.push(this.copySDFMat_);
+
+    this.totalTime_ = 0;
+    this.onWindowResize_();
+  }
+
+  onWindowResize_() {
+    const dpr = window.devicePixelRatio;
+    const canvas = this.threejs_.domElement;
+    canvas.style.width = window.innerWidth + 'px';
+    canvas.style.height = window.innerHeight + 'px';
+    const w = canvas.clientWidth;
+    const h = canvas.clientHeight;
+
+    this.threejs_.setSize(w * dpr, h * dpr, false);
+    for (let m of this.materials_) {
+      m.uniforms.resolution.value.set(w * dpr, h * dpr);
+    }
+
+    this.copySDFMat_.uniforms.resolution.value.set(SCENE_RES.x, SCENE_RES.y);
+    this.radianceFieldMat_.uniforms.resolution.value.set(
+        CASCADE_RES.x / cascade0_Dims, CASCADE_RES.y / cascade0_Dims);
+  }
+
+  raf_() {
+    requestAnimationFrame((t) => {
+      if (this.previousRAF_ === null) {
+        this.previousRAF_ = t;
+      }
+
+      this.step_(t - this.previousRAF_);
+      this.render_();
+      this.raf_();
+      this.previousRAF_ = t;
+    });
+  }
+
+  render_() {
+    const coords = this.lazyBrush_.getBrushCoordinates();
+    this.sceneMaterial_.uniforms.brushPos.value = new THREE.Vector2(
+        coords.x / SCENE_RES.x, 1 - coords.y / SCENE_RES.y);
+    this.sceneMaterial_.uniforms.brushRadius.value = this.params_.brush.radius.radius;
+    this.sceneMaterial_.uniforms.brushColour.value = new THREE.Vector3(
+        this.params_.brush.colour.colour.r,
+        this.params_.brush.colour.colour.g,
+        this.params_.brush.colour.colour.b);
+    this.sceneMaterial_.uniforms.sdfTexture.value = this.sdfTargets_[(this.sdfIndex_ + 1) % 2].texture;
+    this.sceneMaterial_.uniforms.resolution.value = new THREE.Vector2(SCENE_RES.x, SCENE_RES.y);
+    this.threejs_.setRenderTarget(this.sdfFinalTarget_);
+    this.threejs_.render(this.scene_, this.camera_);
+
+    // Create radiance cascades
+    // These are stored separately for debugging.
+    for (let i = 0; i < this.cascadeRealTargets_.length; i++) {
+      const cascadeLevel = i;
+
+      this.cascadeMaterial_.uniforms.cascadeLevel.value = cascadeLevel;
+      this.cascadeMaterial_.uniforms.sdfTexture.value = this.sdfFinalTarget_.texture;
+      this.threejs_.setRenderTarget(this.cascadeRealTargets_[i]);
+      this.threejs_.render(this.cascadeScene_, this.camera_);
+    }
+
+    // merged5 RT0 = cascade5 + merged6
+    // merged4 RT1 = cascade4 + merged5 RT0
+    // merged3 RT0 = cascade3 + merged4 RT1
+
+    // Merge radiance cascades
+    for (let i = this.cascadeRealTargets_.length - 1; i >= 0; i--) {
+      this.cascadeMergeMaterial_.uniforms.currentCascadeLevel.value = i;
+      this.cascadeMergeMaterial_.uniforms.cascadeRealTexture.value = this.cascadeRealTargets_[i].texture;
+      this.cascadeMergeMaterial_.uniforms.sdfTexture.value = this.sdfFinalTarget_.texture;
+      this.cascadeMergeMaterial_.needsUpdate = true;
+      if (i < this.cascadeRealTargets_.length - 1) {
+        this.cascadeMergeMaterial_.uniforms.nextCascadeMergedTexture.value = this.cascadeMergeTargets_[i + 1].texture;
+      }
+      if (i > 0) {
+        this.cascadeMergeMaterial_.uniforms.prevCascadeTexture.value = this.cascadeRealTargets_[i - 1].texture;
+      }
+
+      this.threejs_.setRenderTarget(this.cascadeMergeTargets_[i]);
+      this.threejs_.render(this.cascadeMergeScene_, this.camera_);
+    }
+
+    // Generate radiance field
+    this.radianceFieldMat_.uniforms.mergedCascade0Texture.value = this.cascadeMergeTargets_[0].texture;
+    this.radianceFieldMat_.uniforms.resolution.value = new THREE.Vector2(
+        this.radianceFieldTarget_.width, this.radianceFieldTarget_.height);
+    this.threejs_.setRenderTarget(this.radianceFieldTarget_);
+    this.threejs_.render(this.radianceFieldScene_, this.camera_);
+
+    // Final copy to screen
+    this.finalComposeMat_.uniforms.radianceTexture.value = this.radianceFieldTarget_.texture;
+    this.finalComposeMat_.uniforms.sdfTexture.value = this.sdfFinalTarget_.texture;
+    this.threejs_.setRenderTarget(null);
+    this.threejs_.render(this.finalComposeScene_, this.camera_);
+  }
+
+  step_(timeElapsed) {
+    const timeElapsedS = timeElapsed * 0.001;
+    this.totalTime_ += timeElapsedS;
+
+    for (let m of this.materials_) {
+      m.uniforms.time.value = this.totalTime_;
+    }
+
+    this.updateBrush_(timeElapsed);
+    this.stats_.update();
+  }
+
+  updateBrush_(_) {
+    const hasChanged = this.lazyBrush_.update(
+      { x: this.brushCoords_.x, y: this.brushCoords_.y },
+      { friction: this.brushCoords_.touching ? this.params_.brush.friction.friction / 100 : 1 }
+    )
+    const isDisabled = !this.lazyBrush_.isEnabled();
+    const hasMoved = this.lazyBrush_.brushHasMoved();
+  
+    if (!hasMoved) {
+      // return
+    }
+  
+    if (!this.brushCoords_.touching) {
+      return;
+    }
+  
+    this.brushCoords_.points.push(this.lazyBrush_.getBrushCoordinates());
+    this.brushCoords_.points.push(this.lazyBrush_.getBrushCoordinates());
+
+    this.updateBrushSDF_();
+  }
+
+  updateBrushSDF_() {
+    const p = this.lazyBrush_.getBrushCoordinates();
+
+    if (this.brushCoords_.current == null) {
+      this.brushCoords_.current = { x: p.x, y: p.y };
+      this.brushCoords_.previous = { x: p.x, y: p.y };
+    }
+
+    this.brushCoords_.current = { x: p.x, y: p.y };
+
+    const p1 = this.brushCoords_.current;
+    const p2 = this.brushCoords_.previous;
+
+    this.threejs_.setRenderTarget(this.sdfTargets_[this.sdfIndex_]);
+    this.copySDFMat_.uniforms.brushPos1.value = new THREE.Vector2(
+        p1.x / SCENE_RES.x, 1 - p1.y / SCENE_RES.y);
+    this.copySDFMat_.uniforms.brushPos2.value = new THREE.Vector2(
+        p2.x / SCENE_RES.x, 1 - p2.y / SCENE_RES.y);
+    this.copySDFMat_.uniforms.brushRadius.value = this.params_.brush.radius.radius;
+    this.copySDFMat_.uniforms.brushColour.value = new THREE.Vector3(
+        this.params_.brush.colour.colour.r,
+        this.params_.brush.colour.colour.g,
+        this.params_.brush.colour.colour.b);
+    this.copySDFMat_.uniforms.sdfSource.value = this.sdfTargets_[1 - this.sdfIndex_].texture;
+    this.threejs_.render(this.copySDFScene_, this.camera_);
+    this.sdfIndex_ = (this.sdfIndex_ + 1) % 2;
+
+    this.brushCoords_.previous = this.brushCoords_.current;
+  }
+}
+
+
+let APP_ = null;
+
+window.addEventListener('DOMContentLoaded', async () => {
+  APP_ = new SimonDevGLSLCourse();
+  await APP_.initialize();
+});

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>SimonDev GLSL Shader Course</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="./demo.js" type="module"></script>
+</body>
+</html>

+ 23 - 0
package.json

@@ -0,0 +1,23 @@
+{
+  "name": "package",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "predeploy": "npm run build",
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "vite": "^4.4.9"
+  },
+  "dependencies": {
+    "lazy-brush": "^2.0.1",
+    "three": "^0.156.0",
+    "tweakpane": "^4.0.3",
+    "vite-plugin-solid": "^2.7.0",
+    "vite-plugin-top-level-await": "^1.3.0",
+    "vite-plugin-wasm": "^3.2.2"
+  }
+}

+ 266 - 0
shaders/cascades.glsl

@@ -0,0 +1,266 @@
+
+uniform int cascade0_Dims;
+uniform float cascade0_Range;
+uniform vec2 cascadeResolution;
+
+
+struct CascadeInfo {
+  int dimensions;
+  int angles;
+  int level;
+  vec2 range;
+};
+
+struct PixelIndex {
+  ivec2 index;
+};
+
+struct ProbeIndex {
+  ivec2 index;
+};
+
+struct ProbeAABB {
+  vec2 min;
+  vec2 max;
+  vec2 center;
+};
+
+vec2 _CalculateRange(int cascadeLevel) {
+  const float factor = 4.0;
+ 
+  float cascadeN_Start_Pixels = cascade0_Range * (1.0 - pow(factor, float(cascadeLevel))) / (1.0 - factor);
+  float cascadeN_End_Pixels = cascade0_Range * (1.0 - pow(factor, float(cascadeLevel) + 1.0)) / (1.0 - factor);
+
+  return vec2(cascadeN_Start_Pixels, cascadeN_End_Pixels);
+}
+
+
+CascadeInfo Cascade_GetInfo(int cascadeLevel) {
+  int dims = cascade0_Dims * (1 << cascadeLevel);
+  vec2 range = _CalculateRange(cascadeLevel);
+  return CascadeInfo(dims, dims * dims, cascadeLevel, range);
+}
+
+ProbeIndex ProbeIndex_Create(vec2 pixelIndex, CascadeInfo info) {
+  //fixme
+  return ProbeIndex(ivec2(floor(pixelIndex / float(info.dimensions))));
+}
+
+
+ProbeAABB ProbeAABB_Create(ProbeIndex cascadeIndex, CascadeInfo info) {
+  //fixme
+  float dimensions = float(info.dimensions);
+  vec2 bl = dimensions * vec2(cascadeIndex.index);
+
+  return ProbeAABB(bl, bl + dimensions - 1.0, bl + 0.5 * (dimensions - 1.0));
+}
+
+float _AngleOffset(CascadeInfo info) {
+  //fixme
+  float angleStep = 2.0 * PI / float(info.dimensions * info.dimensions);
+  return angleStep * 0.5;
+}
+
+int _Angle_to_Index(float angle, CascadeInfo info) {
+  float angleNormalized = (angle / (2.0 * PI));
+  //fixme
+  int angleIndex = int(floor(angleNormalized * float(info.dimensions * info.dimensions)));
+
+  return angleIndex % int(info.dimensions * info.dimensions);
+}
+
+struct CascadePixelIndex {
+  ivec2 index;
+};
+
+CascadePixelIndex _Index_To_CascadeIndex(int angleIndex, CascadeInfo info) {
+  angleIndex = angleIndex % (info.dimensions * info.dimensions);
+  int x = angleIndex % info.dimensions;
+  int y = angleIndex / info.dimensions;
+  return CascadePixelIndex(ivec2(x, y));
+}
+
+CascadePixelIndex[4] Cascade_FindNearbyAngles(float angle, CascadeInfo info) {
+  int index = _Angle_to_Index(angle + _AngleOffset(info), info);
+
+  CascadePixelIndex cascadeIndex1 = _Index_To_CascadeIndex(index - 1, info);
+  CascadePixelIndex cascadeIndex2 = _Index_To_CascadeIndex(index, info);
+  CascadePixelIndex cascadeIndex3 = _Index_To_CascadeIndex(index + 1, info);
+  CascadePixelIndex cascadeIndex4 = _Index_To_CascadeIndex(index + 2, info);
+
+  return CascadePixelIndex[4](cascadeIndex1, cascadeIndex2, cascadeIndex3, cascadeIndex4);
+}
+
+CascadePixelIndex[5] Cascade_FindNearbyAngles2(float angle, CascadeInfo info) {
+  int index = _Angle_to_Index(angle + _AngleOffset(info), info);
+
+  int dims = info.dimensions * info.dimensions;
+  CascadePixelIndex cascadeIndex0 = _Index_To_CascadeIndex(index + dims - 2, info);
+  CascadePixelIndex cascadeIndex1 = _Index_To_CascadeIndex(index + dims - 1, info);
+  CascadePixelIndex cascadeIndex2 = _Index_To_CascadeIndex(index, info);
+  CascadePixelIndex cascadeIndex3 = _Index_To_CascadeIndex(index + 1, info);
+  CascadePixelIndex cascadeIndex4 = _Index_To_CascadeIndex(index + 2, info);
+
+  return CascadePixelIndex[5](cascadeIndex0, cascadeIndex1, cascadeIndex2, cascadeIndex3, cascadeIndex4);
+}
+
+CascadePixelIndex CascadePixelIndex_FromPixelIndex(vec2 idx, ProbeAABB aabb) {
+  return CascadePixelIndex(ivec2(idx - aabb.min));
+}
+
+CascadePixelIndex CascadeIndex_FromAngle(float angle, CascadeInfo info) {
+  int index = _Angle_to_Index(angle + _AngleOffset(info), info);
+  return _Index_To_CascadeIndex(index, info);
+}
+
+int _CascadeIndex_to_Index(CascadePixelIndex idx, CascadeInfo info) {
+  //fixme
+  return idx.index.x + idx.index.y * info.dimensions;
+}
+
+float _Index_to_Angle(int index, CascadeInfo info) {
+  //fixme
+  // return (index / float(info.dimensions * info.dimensions)) * 2.0 * PI;
+
+  float angleStep = 2.0 * PI / float(info.dimensions * info.dimensions);
+
+  return float(index) * angleStep;
+}
+
+
+float Angle_FromCascadeIndex(CascadePixelIndex coords, CascadeInfo info) {
+  int ni = _CascadeIndex_to_Index(coords, info);
+  float angle = _Index_to_Angle(ni, info) - _AngleOffset(info);
+  
+  angle = mod(angle, 2.0 * PI);
+
+  return angle;
+}
+
+// vec3 Cascade_BL_and_AngleIndex_To_UV(vec2 cascade_BL_Pixels, vec2 angleIndex, float cascadeLevel, vec2 cascadeResolution) {
+//   return vec3((cascade_BL_Pixels + angleIndex + 0.5) / cascadeResolution, cascadeLevel);
+// }
+
+vec2 Cascade_GenerateUVs(ProbeAABB aabb, CascadePixelIndex cascadeIndex, vec2 cascadeResolution) {
+  vec2 uv = (aabb.min + vec2(cascadeIndex.index) + 0.5) / cascadeResolution;
+  return uv;
+}
+
+struct BilinearPositions {
+  vec2 bl;
+  vec2 br;
+  vec2 tl;
+  vec2 tr;
+  vec2 weight;
+};
+
+BilinearPositions FindBilinearPositions(vec2 pixelIndex, CascadeInfo info) {
+  //fixme
+  vec2 pos = pixelIndex - float(info.dimensions) * 0.5;
+
+  ProbeIndex cascade_BL_Index = ProbeIndex_Create(pos, info);
+  ProbeAABB cascadeAABB = ProbeAABB_Create(cascade_BL_Index, info);
+  vec2 cascade_BL_Pixels = cascadeAABB.center;
+
+  vec2 st = (pixelIndex - cascade_BL_Pixels) / float(info.dimensions);
+  vec2 f = fract(st);
+
+  return BilinearPositions(
+      cascade_BL_Pixels + vec2(0.0, 0.0),
+      cascade_BL_Pixels + vec2(info.dimensions, 0.0),
+      cascade_BL_Pixels + vec2(0.0, info.dimensions),
+      cascade_BL_Pixels + vec2(info.dimensions, info.dimensions),
+      f);
+}
+
+vec4 SampleRadiance_SDF(sampler2D sdfTexture, vec2 sceneResolution, vec2 rayOrigin, vec2 rayDirection, CascadeInfo info) {
+  vec2 ray = rayOrigin;
+
+  float start = info.range.x;
+  float end = info.range.y;
+
+  float stepSize = 0.5;
+  float t = start;
+  for (float i = 0.0; i < 64.0; ++i) {
+    vec2 currentPosition = rayOrigin + t * rayDirection;
+
+    if (t > end) {
+      break;
+    }
+
+    if (currentPosition.x < 0.0 || currentPosition.x > sceneResolution.x - 1.0 ||
+        currentPosition.y < 0.0 || currentPosition.y > sceneResolution.y - 1.0) {
+      break;
+    }
+
+    // Sample the scene SDF, if we're inside the scene, break
+    vec4 sceneSample = texture2D(sdfTexture, (currentPosition + 0.5) / sceneResolution);
+
+    float sceneDist = sceneSample.w;
+    vec3 sceneColour = sceneSample.xyz;
+    if (sceneDist > 0.1) {
+      t += sceneDist;
+
+      continue;
+    }
+
+    return vec4(sceneColour, 0.0);
+  }
+
+  return vec4(vec3(0.0), 1.0);
+}
+
+vec4 SampleRadianceCascadeInDirection(sampler2D mergedTexture, ProbeIndex cascadeIndex, int level, float angleRadians) {
+  CascadeInfo info = Cascade_GetInfo(level);
+  ProbeAABB cascadeAABB = ProbeAABB_Create(cascadeIndex, info);
+  vec2 cascade_Center_Pixels = cascadeAABB.center;
+  vec2 cascade_BL_Pixels = cascadeAABB.min;
+
+  // Quick check for offscreen
+  if (cascade_Center_Pixels.x < 0.0 || cascade_Center_Pixels.x >= cascadeResolution.x ||
+      cascade_Center_Pixels.y < 0.0 || cascade_Center_Pixels.y >= cascadeResolution.y) {
+    return vec4(0.0);
+  }
+
+  CascadePixelIndex nearbyAngleIndices[4] = Cascade_FindNearbyAngles(angleRadians, info);
+
+  vec4 radiance = vec4(0.0);
+  for (int i = 0; i < 4; i++) {
+    CascadePixelIndex angleIndex = nearbyAngleIndices[i];
+
+    vec2 uv = Cascade_GenerateUVs(cascadeAABB, angleIndex, cascadeResolution);
+    vec4 radianceSample = texture(mergedTexture, uv);
+    radiance += radianceSample;
+  }
+  radiance *= 0.25;
+
+  return radiance;
+}
+
+
+vec4 SampleMergedRadiance_Bilinear(sampler2D mergedTexture, vec2 pixelIndex, float angleRadians, int level) {
+  CascadeInfo ci = Cascade_GetInfo(level);
+
+  // Sample the radiance from higher cascade levels in direction of angleRadians
+  // Make sure to use the position of the probe from the lower cascade level
+  CascadeInfo ciLower = Cascade_GetInfo(level - 1);
+  ProbeIndex idxLower = ProbeIndex_Create(pixelIndex, ciLower);
+  ProbeAABB aabbLower = ProbeAABB_Create(idxLower, ciLower);
+  BilinearPositions positions = FindBilinearPositions(aabbLower.center, ci);
+
+  ProbeIndex cascadeIndex_x0y0 = ProbeIndex_Create(positions.bl, ci);
+  ProbeIndex cascadeIndex_x1y0 = ProbeIndex_Create(positions.br, ci);
+  ProbeIndex cascadeIndex_x0y1 = ProbeIndex_Create(positions.tl, ci);
+  ProbeIndex cascadeIndex_x1y1 = ProbeIndex_Create(positions.tr, ci);
+
+  vec4 radiance_x0y0 = SampleRadianceCascadeInDirection(mergedTexture, cascadeIndex_x0y0, level, angleRadians);
+  vec4 radiance_x1y0 = SampleRadianceCascadeInDirection(mergedTexture, cascadeIndex_x1y0, level, angleRadians);
+  vec4 radiance_x0y1 = SampleRadianceCascadeInDirection(mergedTexture, cascadeIndex_x0y1, level, angleRadians);
+  vec4 radiance_x1y1 = SampleRadianceCascadeInDirection(mergedTexture, cascadeIndex_x1y1, level, angleRadians);
+
+  vec4 px1 = mix(radiance_x0y0, radiance_x1y0, positions.weight.x);
+  vec4 px2 = mix(radiance_x0y1, radiance_x1y1, positions.weight.x);
+  vec4 radiance = mix(px1, px2, positions.weight.y);
+
+  return radiance;
+}

+ 422 - 0
shaders/common.glsl

@@ -0,0 +1,422 @@
+
+mat2 rotate2D(float angle) {
+  float s = sin(angle);
+  float c = cos(angle);
+  return mat2(c, -s, s, c);
+}
+
+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 easeOut(float x, float t) {
+	return 1.0 - pow(1.0 - x, t);
+}
+
+float easeIn(float x, float t) {
+	return pow(x, t);
+}
+
+float easeOutBounce(float x) {
+  const float n1 = 7.5625;
+  const float d1 = 2.75;
+
+  if (x < 1.0 / d1) {
+    return n1 * x * x;
+  } else if (x < 2.0 / d1) {
+    x -= 1.5 / d1;
+    return n1 * x * x + 0.75;
+  } else if (x < 2.5 / d1) {
+    x -= 2.25 / d1;
+    return n1 * x * x + 0.9375;
+  } else {
+    x -= 2.625 / d1;
+    return n1 * x * x + 0.984375;
+  }
+}
+
+float easeInBounce(float x) {
+  return 1.0 - easeOutBounce(1.0 - x);
+}
+
+float easeInOutBounce(float x) {
+  return x < 0.5
+    ? (1.0 - easeOutBounce(1.0 - 2.0 * x)) / 2.0
+    : (1.0 + easeOutBounce(2.0 * x - 1.0)) / 2.0;
+}
+
+float elasticOut(float t) {
+  const float HALF_PI =1.5707963267948966;
+
+  return sin(-13.0 * (t + 1.0) * HALF_PI) * pow(2.0, -10.0 * t) + 1.0;
+}
+
+float sinc( float x, float k )
+{
+  float a = 3.14159*(k*x-1.0);
+  return sin(a)/a;
+}
+
+float pcurve( float x, float a, float b )
+{
+    float k = pow(a+b,a+b)/(pow(a,a)*pow(b,b));
+    return k*pow(x,a)*pow(1.0-x,b);
+}
+
+float easeOutPow(float x, float k) {
+  return 1.0 - pow(1.0 - x, k);
+}
+
+float easeOutQuad(float x) {
+  return 1.0 - (1.0 - x) * (1.0 - x);
+}
+
+// gain definition
+float gain(float x, float k) 
+{
+  float a = 0.5*pow(2.0*((x<0.5)?x:1.0-x), k);
+  return (x<0.5)?a:1.0-a;
+}
+
+float expImpulse( float x, float k )
+{
+  float h = k * x;
+  return max(0.0, h * exp(1.0 - h));
+}
+
+float cubicPulse( float c, float w, float x )
+{
+    x = abs(x - c);
+    if( x>w ) return 0.0;
+    x /= w;
+    return 1.0 - x*x*(3.0-2.0*x);
+}
+
+float parabola( float x, float k )
+{
+    return pow( 4.0*x*(1.0-x), k );
+}
+
+float smootherstep(float edge0, float edge1, float x) {
+  // Scale, and clamp x to 0..1 range
+  x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+  // Evaluate polynomial
+  return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+vec2 smootherstep2(vec2 edge0, vec2 edge1, vec2 x) {
+  // Scale, and clamp x to 0..1 range
+  x = clamp((x - edge0) / (edge1 - edge0), vec2(0.0), vec2(1.0));
+  // Evaluate polynomial
+  return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+vec3 smootherstep3(vec3 edge0, vec3 edge1, vec3 x) {
+  // Scale, and clamp x to 0..1 range
+  x = clamp((x - edge0) / (edge1 - edge0), vec3(0.0), vec3(1.0));
+  // Evaluate polynomial
+  return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+
+float integralSmoothstep( float x, float T )
+{
+    if( x>T ) return x - T/2.0;
+    return x*x*x*(1.0-x*0.5/T)/T/T;
+}
+
+/////////////////////////////////////////////////////////////////////////
+//
+// 2D SDF's
+//
+/////////////////////////////////////////////////////////////////////////
+
+float opUnion( float d1, float d2 )
+{
+  return min(d1,d2);
+}
+float opSubtraction( float d1, float d2 )
+{
+  return max(-d1,d2);
+}
+float opIntersection( float d1, float d2 )
+{
+  return max(d1,d2);
+}
+float opXor(float d1, float d2 )
+{
+  return max(min(d1,d2),-max(d1,d2));
+}
+
+float sdfCircle(vec2 p, float r) {
+    return length(p) - r;
+}
+
+float sdfBox(vec2 p, vec2 b) {
+  vec2 d = abs(p) - b;
+  return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
+}
+
+float sdEquilateralTriangle( in vec2 p, in float r )
+{
+    const float k = sqrt(3.0);
+    p.x = abs(p.x) - r;
+    p.y = p.y + r/k;
+    if( p.x+k*p.y>0.0 ) p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0;
+    p.x -= clamp( p.x, -2.0*r, 0.0 );
+    return -length(p)*sign(p.y);
+}
+
+float sdfLine(vec2 p, vec2 a, vec2 b) {
+  vec2 pa = p - a;
+  vec2 ba = b - a;
+  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
+
+  return length(pa - ba * h);
+}
+
+float sdArc( in vec2 p, in vec2 sc, in float ra, float rb )
+{
+    // sc is the sin/cos of the arc's aperture
+    p.x = abs(p.x);
+    return ((sc.y*p.x>sc.x*p.y) ? length(p-sc*ra) : 
+                                  abs(length(p)-ra)) - rb;
+}
+
+float sdPie( in vec2 p, in vec2 c, in float r )
+{
+    p.x = abs(p.x);
+    float l = length(p) - r;
+    float m = length(p-c*clamp(dot(p,c),0.0,r)); // c=sin/cos of aperture
+    return max(l,m*sign(c.y*p.x-c.x*p.y));
+}
+
+float sdfStickman(vec2 pos) {
+  float d = sdfCircle(pos - vec2(0.0, 20.0), 20.0);
+  float d1 = sdfLine(pos, vec2(0.0, 0.0), vec2(0.0, -25.0)) - 4.0;
+  float d2 = sdfLine(pos, vec2(0.0, -25.0), vec2(20.0, -50.0)) - 4.0;
+  float d3 = sdfLine(pos, vec2(0.0, -25.0), vec2(-20.0, -50.0)) - 4.0;
+  float d4 = sdfLine(pos, vec2(0.0, -12.5), vec2(-20.0, -12.5)) - 4.0;
+  float d5 = sdfLine(pos, vec2(0.0, -12.5), vec2(20.0, -12.5)) - 4.0;
+
+  d = min(d, d1);
+  d = min(d, d2);
+  d = min(d, d3);
+  d = min(d, d4);
+  d = min(d, d5);
+
+  return d;
+}
+
+float sdPolygon(in vec2[3] v, in vec2 p )
+{
+    float d = dot(p-v[0],p-v[0]);
+    float s = 1.0;
+    for( int i=0, j=3-1; i<3; j=i, i++ )
+    {
+        vec2 e = v[j] - v[i];
+        vec2 w =    p - v[i];
+        vec2 b = w - e*clamp( dot(w,e)/dot(e,e), 0.0, 1.0 );
+        d = min( d, dot(b,b) );
+        bvec3 c = bvec3(p.y>=v[i].y,p.y<v[j].y,e.x*w.y>e.y*w.x);
+        if( all(c) || all(not(c)) ) s*=-1.0;  
+    }
+    return s*sqrt(d);
+}
+
+float sdPolygon(in vec2[4] v, in vec2 p )
+{
+    float d = dot(p-v[0],p-v[0]);
+    float s = 1.0;
+    for( int i=0, j=4-1; i<4; j=i, i++ )
+    {
+        vec2 e = v[j] - v[i];
+        vec2 w =    p - v[i];
+        vec2 b = w - e*clamp( dot(w,e)/dot(e,e), 0.0, 1.0 );
+        d = min( d, dot(b,b) );
+        bvec3 c = bvec3(p.y>=v[i].y,p.y<v[j].y,e.x*w.y>e.y*w.x);
+        if( all(c) || all(not(c)) ) s*=-1.0;  
+    }
+    return s*sqrt(d);
+}
+
+vec3 opRep( in vec3 p, in vec3 c )
+{
+    vec3 q = mod(p+0.5*c,c)-0.5*c;
+    return q;
+}
+
+
+vec3 opRepLim( in vec3 p, in float c, in vec3 l)
+{
+    vec3 q = p-c*clamp(round(p/c),-l,l);
+    return q;
+}
+
+// Create multiple copies of an object - https://iquilezles.org/articles/distfunctions
+vec2 opRepLim( in vec2 p, in float s, in vec2 lima, in vec2 limb )
+{
+    return p-s*clamp(round(p/s),lima,limb);
+}
+
+// Create infinite copies of an object -  https://iquilezles.org/articles/distfunctions
+vec2 opRep( in vec2 p, in float s )
+{
+    return mod(p+s*0.5,s)-s*0.5;
+}
+
+
+
+/////////////////////////////////////////////////////////////////////////
+//
+// 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 col3(v);
+}
+
+float softMax(float a, float b, float k) {
+  return log(exp(k * a) + exp(k * b)) / k;
+}
+
+float softMin(float a, float b, float k) {
+  return -softMax(-a, -b, k);
+}
+
+/////////////////////////////////////////////////////////////////////////
+//
+// Animated SDF's
+//
+/////////////////////////////////////////////////////////////////////////
+float sdfDistLines_Anim(vec2 p, vec2 a, vec2 b, float t) {
+  vec2 ab = (a + b) * 0.5;
+
+  float t1 = smoothstep(0.0, 0.75, t);
+  float d = sdfLine(p, mix(ab, a, t1), mix(ab, b, t1));
+
+  vec2 dir = normalize(b - a);
+
+  // d = min(d, sdfLine(p, b, b - 150.0 * dir + 75.0 * vec2(dir.y, -dir.x)));
+  // d = min(d, sdfLine(p, b, b - 150.0 * dir + 75.0 * vec2(-dir.y, dir.x)));
+
+  float t2 = smoothstep(0.75, 1.0, t);
+  vec2 offset = mix(vec2(0.0), 10.0 * vec2(dir.y, -dir.x), t2);
+  float ends = sdfLine(p, b - offset, b + offset);
+  ends = min(ends, sdfLine(p, a - offset, a + offset));
+
+  float t3 = smoothstep(0.75, 0.76, t);
+  d = mix(d, min(d, ends), t3);
+
+  return d;
+}
+
+float sdfLine_Anim(vec2 p, vec2 a, vec2 b, float t) {
+  vec2 bAnimated = mix(a, b, t);
+
+  vec2 pa = p - a;
+  vec2 ba = bAnimated - a;
+  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
+
+  return length(pa - ba * h);
+}
+
+float sdfLine_Anim_Center(vec2 p, vec2 a, vec2 b, float t) {
+  vec2 center = (a + b) * 0.5;
+
+  a = mix(center, a, t);
+  b = mix(center, b, t);
+
+  vec2 pa = p - a;
+  vec2 ba = b - a;
+  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
+
+  return length(pa - ba * h);
+}
+
+struct SDFArrowParams {
+  vec2 a;
+  vec2 b;
+  float size;
+};
+
+float sdfArrow_Anim(vec2 p, SDFArrowParams params, float t) {
+  vec2 bAnimated = mix(params.a, params.b, t);
+  float d = sdfLine(p, params.a, bAnimated);
+
+  vec2 dir = normalize(params.b - params.a);
+  float t_Arrow = t * t;
+
+  float arrowLen = params.size;
+
+  d = min(d, sdfLine(p, bAnimated, bAnimated - t_Arrow * arrowLen * 0.75 * dir + t_Arrow * arrowLen * 0.25 * vec2(dir.y, -dir.x)));
+  d = min(d, sdfLine(p, bAnimated, bAnimated - t_Arrow * arrowLen * 0.75 * dir + t_Arrow * arrowLen * 0.25 * vec2(-dir.y, dir.x)));
+
+  return d;
+}
+
+float sdfArrow_Anim(vec2 p, vec2 a, vec2 b, float t) {
+  SDFArrowParams params = SDFArrowParams(a, b, 25.0);
+
+  return sdfArrow_Anim(p, params, t);
+}
+
+
+float sdfEye(vec2 p, vec2 pos, float size) {
+  vec2 dir = vec2(0.0, 1.0);
+  float d1 = sdfLine(p, pos, pos + size * dir + size * 0.5 * vec2(-dir.y, dir.x));
+  float d2 = sdfLine(p, pos, pos + size * dir + size * 0.5 * vec2(dir.y, -dir.x));
+
+  float t = 3.14159 / 6.75;
+  float d3 = sdPie(p - pos, vec2(sin(t), cos(t)), size * 0.75);
+
+  float d = min(d1, d2);
+  d = min(d, d3);
+
+  return d;
+}
+
+vec3 aces_tonemap(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 pow(clamp(m2 * (a / b), 0.0, 1.0), vec3(1.0 / 2.2));	
+}
+

+ 33 - 0
shaders/compute-cascade-fragment-shader.glsl

@@ -0,0 +1,33 @@
+varying vec2 vUv;
+
+uniform sampler2D sceneTexture;
+uniform sampler2D sdfTexture;
+uniform vec2 sceneResolution;
+
+uniform int cascadeLevel;
+
+
+
+// #define DEBUG
+
+void main() {
+  vec2 pixelIndex = (gl_FragCoord.xy - 0.5);
+
+  // Grab info about the current cascade level
+  CascadeInfo info = Cascade_GetInfo(cascadeLevel);
+  ProbeIndex cascadeIndex = ProbeIndex_Create(pixelIndex, info);
+  ProbeAABB aabb = ProbeAABB_Create(cascadeIndex, info);
+
+  // Angle of the ray from the center of the cascade
+  CascadePixelIndex coordsInCascade = CascadePixelIndex(ivec2(pixelIndex - aabb.min));
+  float angleRadians = Angle_FromCascadeIndex(coordsInCascade, info);
+
+  // Sample the scene to get radiance
+  vec2 rayDirection = vec2(cos(angleRadians), sin(angleRadians));
+  vec2 rayOrigin = aabb.center * sceneResolution / cascadeResolution;
+
+  vec4 radiance = SampleRadiance_SDF(
+      sdfTexture, sceneResolution, rayOrigin, rayDirection, info);
+
+  gl_FragColor = radiance;
+}

+ 10 - 0
shaders/compute-cascade-vertex-shader.glsl

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

+ 9 - 0
shaders/copy-fragment-shader.glsl

@@ -0,0 +1,9 @@
+varying vec2 vUv;
+
+uniform sampler2D diffuse;
+
+void main() {
+  vec4 texel = texture2D(diffuse, vUv);
+
+  gl_FragColor = texel;
+}

+ 64 - 0
shaders/copy-sdf-fragment-shader.glsl

@@ -0,0 +1,64 @@
+varying vec2 vUv;
+
+uniform sampler2D sdfSource;
+
+uniform vec2 brushPos1;
+uniform vec2 brushPos2;
+uniform float brushRadius;
+uniform vec3 brushColour;
+
+uniform vec2 resolution;
+uniform float time;
+
+void main() {
+  vec2 pixelCoords = (vUv - 0.5) * resolution;
+
+  vec4 texel = texture2D(sdfSource, vUv);
+
+
+  // Kinda dumb but whatever
+  vec2 brushCoords1 = (brushPos1 - 0.5) * resolution;
+  vec2 brushCoords2 = (brushPos2 - 0.5) * resolution;
+
+  float distBetweenBrushes = distance(brushCoords1, brushCoords2);
+  float steps = ceil(distBetweenBrushes);
+
+  for (float i = 0.0; i < steps; ++i) {
+    vec2 brushCoords = mix(brushCoords1, brushCoords2, i / steps);
+    float dist = sdfCircle(pixelCoords - brushCoords, brushRadius * 0.5);
+
+    texel.w = min(texel.w, dist);
+    texel.xyz = mix(texel.xyz, brushColour, smoothstep(1.0, 0.0, dist));
+  }
+
+  gl_FragColor = texel;
+
+  // float lightDist1 = sdfBox(pixelCoords - vec2(-16.0, 0.0), vec2(8.0, 64.0));
+  // float lightDist2 = sdfBox(pixelCoords - vec2(-48.0, 0.0), vec2(8.0, 80.0));
+  // float lightDist3 = sdfBox(pixelCoords - vec2(48.0, 0.0), vec2(8.0, 80.0));
+  // float lightDist4 = sdfBox(pixelCoords - vec2(0.0, -200.0), vec2(200.0, 8.0));
+
+  // float opaqueDist1 = sdfBox(pixelCoords - vec2(16.0, 24.0), vec2(8.0, 16.0));
+  // float opaqueDist2 = sdfBox(pixelCoords - vec2(16.0, -24.0), vec2(8.0, 16.0));
+  // float opaqueDist3 = sdfBox(pixelCoords - vec2(0.0, 150.0), vec2(remap(sin(time), -1.0, 1.0, 100.0, 200.0), 16.0));
+  // // float opaqueDist3 = sdBox(pixelCoords - vec2(0.0, 150.0), vec2(200.0, 16.0));
+
+  // float sd = min(opaqueDist1, opaqueDist2);
+  // sd = min(sd, opaqueDist3);
+
+  // vec3 emissivity = col3(0.0);
+
+  // emissivity = mix(emissivity, col3(1.0, 1.0, 0.0), smoothstep(1.0, 0.0, lightDist1));
+  // sd = min(sd, lightDist1);
+
+  // emissivity = mix(emissivity, col3(0.8, 0.9, 1.0), smoothstep(1.0, 0.0, lightDist4));
+  // sd = min(sd, lightDist4);
+
+  // // sd = min(sd, lightDist2);
+  // // emissivity = mix(emissivity, col3(1.0, 0.0, 0.0), smoothstep(1.0, 0.0, lightDist2));
+
+  // sd = min(sd, lightDist3);
+  // emissivity = mix(emissivity, col3(0.0, 0.0, 1.0), smoothstep(1.0, 0.0, lightDist3));
+
+  // gl_FragColor = vec4(emissivity, sd);
+}

+ 10 - 0
shaders/copy-vertex-shader.glsl

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

+ 63 - 0
shaders/final-compose-fragment-shader.glsl

@@ -0,0 +1,63 @@
+varying vec2 vUv;
+
+uniform sampler2D radianceTexture;
+uniform sampler2D sceneTexture;
+uniform sampler2D sdfTexture;
+uniform vec2 resolution;
+
+vec3 BackgroundColour() {
+  return col3(1.0);
+}
+
+vec3 drawGrid(vec2 pixelCoords, vec3 colour, vec3 lineColour, float cellSpacing, float lineWidth, float pixelSize) {
+  vec2 cellPosition = abs(fract(pixelCoords / vec2(cellSpacing)) - 0.5);
+  float distToEdge = (0.5 - max(cellPosition.x, cellPosition.y)) * cellSpacing;
+  float lines = smoothstep(lineWidth - pixelSize, lineWidth, distToEdge);
+
+  colour = mix(lineColour, colour, lines);
+
+  return colour;
+}
+
+vec3 drawGraphBackground_Ex(vec2 pixelCoords, float scale) {
+  float pixelSize = 1.0 / scale;
+  vec2 cellPosition = floor(pixelCoords / vec2(100.0));
+  vec2 cellID = vec2(floor(cellPosition.x), floor(cellPosition.y));
+  vec3 checkerboard = col3(mod(cellID.x + cellID.y, 2.0));
+
+  vec3 colour = BackgroundColour();
+  colour = mix(colour, checkerboard, 0.05);
+
+  colour = drawGrid(pixelCoords, colour, col3(0.5), 10.0, 1.0, pixelSize);
+  colour = drawGrid(pixelCoords, colour, col3(0.25), 100.0, 2.5, pixelSize);
+  colour = (col3(0.95) + hash(pixelCoords) * 0.01) * colour;
+
+  return colour;
+}
+
+vec3 drawGraphBackground(vec2 pixelCoords) {
+  return drawGraphBackground_Ex(pixelCoords, 1.0);
+}
+
+void main() {
+  vec2 pixelCoords = (vUv - 0.5) * resolution;
+
+  vec2 uv = vUv;
+
+  vec4 radiance = texture(radianceTexture, uv);
+
+  vec4 scene = texture2D(sceneTexture, uv);
+  vec4 sdf = texture2D(sdfTexture, uv);
+  vec3 bg = drawGraphBackground(pixelCoords);
+
+  vec3 colour = mix(bg, col3(sdf.xyz), smoothstep(1.0, 0.0, sdf.w));
+
+#if defined(USE_OKLAB)
+  colour = oklabToRGB(colour);
+#endif
+
+  colour *= radiance.xyz;
+  colour = aces_tonemap(colour);
+
+  gl_FragColor = vec4(colour, 1.0);
+}

+ 10 - 0
shaders/header.glsl

@@ -0,0 +1,10 @@
+precision highp float;
+precision highp sampler2D;
+precision highp sampler2DArray;
+precision highp sampler3D;
+precision highp int;
+
+#define USE_OKLAB
+
+const float PI  = 3.14159265359;
+const float TAU = 6.28318530718;

+ 54 - 0
shaders/merge-cascades-fragment-shader.glsl

@@ -0,0 +1,54 @@
+varying vec2 vUv;
+
+// uniform sampler2DArray cascadeTextures;
+uniform sampler2D nextCascadeMergedTexture;
+uniform sampler2D cascadeRealTexture;
+uniform sampler2D sceneTexture;
+uniform sampler2D sdfTexture;
+uniform vec2 sceneResolution;
+
+uniform int numCascadeLevels;
+uniform int currentCascadeLevel;
+
+
+void main() {
+  vec2 pixelCoords = gl_FragCoord.xy * cascadeResolution - cascadeResolution * 0.5;
+  vec2 pixelIndex = (gl_FragCoord.xy - 0.5);
+
+  // Debug
+  CascadeInfo ci = Cascade_GetInfo(currentCascadeLevel);
+  ProbeIndex cascadeIndex = ProbeIndex_Create(pixelIndex, ci);
+  ProbeAABB cascadeAABB = ProbeAABB_Create(cascadeIndex, ci);
+
+  CascadePixelIndex coordsInCascade = CascadePixelIndex(ivec2(pixelIndex - cascadeAABB.min));
+  float angleRadians = Angle_FromCascadeIndex(coordsInCascade, ci);
+
+  // This is the computed radiance, in the direction "angleRadians"
+  vec4 radiance = texture(cascadeRealTexture, gl_FragCoord.xy / cascadeResolution);
+
+  // Sample the scene to get radiance
+  vec2 rayDirection = vec2(cos(angleRadians), sin(angleRadians));
+  vec2 rayOrigin = cascadeAABB.center * sceneResolution / cascadeResolution;
+
+  // merged5 RT0 = cascade5 + merged6
+  // merged4 RT1 = cascade4 + merged5 RT0
+  // merged3 RT0 = cascade3 + merged4 RT1
+
+  int lastCascadeIndex = numCascadeLevels - 1;
+  int nextCascadeIndex = currentCascadeLevel + 1;
+
+  if (nextCascadeIndex <= lastCascadeIndex) {
+#if defined(RADIANCE_MERGE_13TAP)
+    vec4 radianceSample = SampleMergedRadiance_13tap(
+        pixelIndex, angleRadians, nextCascadeIndex);
+#else
+    vec4 radianceSample = SampleMergedRadiance_Bilinear(
+        nextCascadeMergedTexture, pixelIndex, angleRadians, nextCascadeIndex);
+#endif
+
+    radiance.rgb += radianceSample.rgb * radiance.a;
+    radiance.a *= radianceSample.a;
+  }
+
+  gl_FragColor = radiance;
+}

+ 10 - 0
shaders/merge-cascades-vertex-shader.glsl

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

+ 22 - 0
shaders/noise.glsl

@@ -0,0 +1,22 @@
+
+
+
+// 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) );
+}

+ 51 - 0
shaders/oklab.glsl

@@ -0,0 +1,51 @@
+/////////////////////////////////////////////////////////////////////////
+//
+// OKLab hacky code
+//
+/////////////////////////////////////////////////////////////////////////
+
+const mat3 fwdA = mat3(1.0, 1.0, 1.0,
+                       0.3963377774, -0.1055613458, -0.0894841775,
+                       0.2158037573, -0.0638541728, -1.2914855480);
+                       
+const mat3 fwdB = mat3( 4.0767245293, -1.2681437731, -0.0041119885,
+                       -3.3072168827, 2.6093323231, -0.7034763098,
+                        0.2307590544, -0.3411344290,  1.7068625689);
+
+const mat3 invB = mat3(0.4121656120, 0.2118591070, 0.0883097947,
+                       0.5362752080, 0.6807189584, 0.2818474174,
+                       0.0514575653, 0.1074065790, 0.6302613616);
+                       
+const mat3 invA = mat3( 0.2104542553, 1.9779984951, 0.0259040371,
+                        0.7936177850, -2.4285922050, 0.7827717662,
+                       -0.0040720468, 0.4505937099, -0.8086757660);
+
+vec3 rgbToOklab(vec3 c) {
+  vec3 lms = invB * c;
+
+  return (sign(lms)*pow(abs(lms), vec3(0.3333333333333)));   
+}
+
+vec3 oklabToRGB(vec3 c) {
+  vec3 lms = c;
+  
+  return fwdB * (lms * lms * lms);    
+}
+
+
+
+#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

+ 35 - 0
shaders/radiance-field-fragment-shader.glsl

@@ -0,0 +1,35 @@
+varying vec2 vUv;
+
+// // DEBUG
+// uniform sampler2DArray cascadeTextures;
+// //
+
+uniform sampler2D mergedCascade0Texture;
+
+uniform vec2 sceneResolution;
+
+
+void main() {
+  CascadeInfo ci0 = Cascade_GetInfo(0);
+
+  vec2 pixelIndex = (gl_FragCoord.xy - 0.5);
+  //fixme
+  vec2 pixelIndexCascadeTexture = pixelIndex * float(ci0.dimensions);
+
+  ProbeIndex cascade0_Index = ProbeIndex_Create(pixelIndexCascadeTexture, ci0);
+  ProbeAABB cascade0_AABB = ProbeAABB_Create(cascade0_Index, ci0);
+  vec2 cascade0_BL_Pixels = cascade0_AABB.min;
+
+  vec4 radiance = vec4(0.0);
+  for (int i = 0; i < ci0.dimensions; ++i) {
+    for (int j = 0; j < ci0.dimensions; ++j) {
+      vec2 sampleIndex = cascade0_BL_Pixels + vec2(i, j);
+      radiance += texture(mergedCascade0Texture, (sampleIndex + 0.5) / cascadeResolution);
+    }
+  }
+
+  //fixme
+  radiance /= float(ci0.dimensions * ci0.dimensions);
+
+  gl_FragColor = radiance;
+}

+ 10 - 0
shaders/radiance-field-vertex-shader.glsl

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

+ 37 - 0
shaders/scene-fragment-shader.glsl

@@ -0,0 +1,37 @@
+
+uniform vec2 resolution;
+
+varying vec3 vWorldPosition;
+varying vec3 vWorldNormal;
+varying vec2 vUv;
+
+uniform float time;
+uniform sampler2D sdfTexture;
+
+uniform vec2 brushPos;
+uniform float brushRadius;
+uniform vec3 brushColour;
+
+
+void main() {
+  vec2 uv = gl_FragCoord.xy / resolution.xy;
+  vec2 pixelCoords = gl_FragCoord.xy - resolution.xy / 2.0;
+
+  vec4 texel = texture2D(sdfTexture, uv);
+
+  vec3 lightColour = vec3(0.0, 0.0, 1.0);
+  float lightDist = sdfBox(pixelCoords - vec2(0.0), vec2(20.0));
+
+  vec3 colour = mix(texel.xyz, lightColour, smoothstep(0.0, 1.0, texel.w));
+  float dist = min(texel.w, lightDist);
+
+  // Draw temporary brush
+  vec2 brushCoords = (brushPos - 0.5) * resolution;
+  float brushDist = sdfCircle((pixelCoords - brushCoords), brushRadius * 0.5);
+
+  colour = mix(colour, brushColour, smoothstep(1.0, 0.0, brushDist));
+  dist = min(dist, brushDist);
+
+
+  gl_FragColor = vec4(colour, dist);
+}

+ 15 - 0
shaders/scene-vertex-shader.glsl

@@ -0,0 +1,15 @@
+varying vec3 vWorldPosition;
+varying vec3 vWorldNormal;
+varying vec2 vUv;
+
+
+void main() {
+  vec4 localSpacePosition = vec4(position, 1.0);
+  vec4 worldPosition = modelMatrix * localSpacePosition;
+
+  vWorldPosition = worldPosition.xyz;
+  vWorldNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz);
+  vUv = uv;
+
+  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+}

+ 37 - 0
vite.config.js

@@ -0,0 +1,37 @@
+import { defineConfig } from 'vite';
+import wasm from 'vite-plugin-wasm';
+import topLevelAwait from 'vite-plugin-top-level-await';
+import solidPlugin from 'vite-plugin-solid';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+    plugins: [
+        wasm(),
+        topLevelAwait(),
+        solidPlugin(),
+        {
+            name: 'reload-glsl',
+            handleHotUpdate({ file, server }) {
+              if (file.endsWith('.glsl')) {
+                server.ws.send({
+                  type: 'full-reload'
+                });
+              }
+            }
+        }
+    ],
+    resolve: {
+        alias: {
+        },
+    },
+    build: {
+        sourcemap: true,
+    },
+    server: {
+        port: 5200,
+        hmr: {
+            clientPort: 5200,
+        }
+    },
+    base: "/Quick_Grass/"
+});