Back to Safe Sand · Material studies
SOURCE / PINNED RELEASE

Made of little things.

Safe Sand · Material studies

Release
9101a9cd9fb4…
Author-recorded commit
75d98e1b8edb…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-7682e55a791

Archive hash verified: 6c8ddabeb31100e1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/engine/render.js
// Adapted from av/safe-sand, e27bafa. See THIRD_PARTY.md for provenance.
import { program, texture, framebuffer, setUniforms, bindTex } from './gl.js';
import * as S from './render-shaders.js';
import { TW } from './sim.js';

export class Renderer {
  constructor(gl) {
    this.gl = gl;
    this.vao = gl.createVertexArray();
    this.progs = {
      shadow: program(gl, S.SHADOW_VS, S.SHADOW_FS, 'shadow'),
      blur: program(gl, S.QUAD_VS, S.BLUR_FS, 'blur'),
      bg: program(gl, S.QUAD_VS, S.BG_FS, 'bg'),
      grain: program(gl, S.GRAIN_VS, S.GRAIN_FS, 'grain'),
    };
    this.maxPoint = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE)[1];
    this.screenTex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this.screenTex);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([240, 240, 240, 255]));
    this.look = { screenGain: 1, shadowK: 0.42, aoK: 0.28, room: [0.009, 0.009, 0.0102], amb: 1.15, key: 1.0, sparkle: 1, transl: 1, floor: [0.3, 0.85, 0.5], cull: 3.0, sq: 0.5 };
  }

  resize(w, h, shadowScale) {
    const gl = this.gl;
    this.shadowTex?.forEach(t => gl.deleteTexture(t));
    this.shadowFb?.forEach(f => gl.deleteFramebuffer(f));
    this.w = w; this.h = h;
    this.sw = Math.max(16, Math.round(w * shadowScale));
    this.sh = Math.max(16, Math.round(h * shadowScale));
    this.shadowTex = [0, 1].map(() => texture(gl, this.sw, this.sh, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, gl.LINEAR));
    this.shadowFb = this.shadowTex.map((t) => framebuffer(gl, [t]));
  }

  paletteMean(pal) {
    const m = [0, 0, 0];
    let prev = 0;
    for (let k = 0; k < 8; k++) {
      const w = Math.max(pal[k * 4 + 3] - prev, 0);
      prev = Math.max(prev, pal[k * 4 + 3]);
      for (let c = 0; c < 3; c++) m[c] += pal[k * 4 + c] * w;
    }
    return m;
  }

  setScreen(canvas) {
    const gl = this.gl;
    gl.bindTexture(gl.TEXTURE_2D, this.screenTex);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
  }

  draw(sim, light, camera) {
    const gl = this.gl;
    const L = light;
    const common = { ...sim.presetUniforms(), uTW: { type: 'i', v: TW }, uWorld: sim.world, uL: L,
      uCamera: camera.basis(), uCameraScale: camera.scale(sim.world), uCameraDepth: Math.max(...sim.world) + 240,
      uViewLight: camera.rotate([L[0], -L[1], L[2]]).map((v, i) => i === 1 ? -v : v),
    };
    gl.bindVertexArray(this.vao);

    const sp = this.progs.shadow;
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFb[0]);
    gl.viewport(0, 0, this.sw, this.sh);
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    gl.useProgram(sp.p);
    bindTex(gl, sp, 'uA', sim.A, 0);
    bindTex(gl, sp, 'uB', sim.B, 1);
    bindTex(gl, sp, 'uG0', sim.G0, 2);
    setUniforms(gl, sp, { ...common, uRes: [this.sw, this.sh] });
    gl.drawArrays(gl.POINTS, 0, sim.active);
    gl.disable(gl.BLEND);

    const bp = this.progs.blur;
    gl.useProgram(bp.p);
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFb[1]);
    bindTex(gl, bp, 'uT', this.shadowTex[0], 0);
    setUniforms(gl, bp, { uDir: [1, 0] });
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.shadowFb[0]);
    bindTex(gl, bp, 'uT', this.shadowTex[1], 0);
    setUniforms(gl, bp, { uDir: [0, 1] });
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, this.w, this.h);
    gl.disable(gl.DEPTH_TEST);
    gl.depthMask(false);
    const look = this.look;
    const bgp = this.progs.bg;
    const fs = Math.pow(sim.preset.size[0] / 0.19, 3);
    gl.useProgram(bgp.p);
    bindTex(gl, bgp, 'uScreen', this.screenTex, 0);
    bindTex(gl, bgp, 'uShadow', this.shadowTex[0], 1);
    bindTex(gl, bgp, 'uG0', sim.G0, 2);
    setUniforms(gl, bgp, {
      ...common,
      uRes: [this.w, this.h],
      uBg: [look.screenGain, look.shadowK, look.aoK, 0],
      uRoom: look.room,
      uLook: [look.amb, look.key, look.sparkle, look.transl],
      uPm: this.paletteMean(sim.preset.palette),
      uFloor: [1.6 * sim.preset.size[0], look.floor[0] * fs, look.floor[1] * fs, look.floor[2]],
    });
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);
    gl.depthMask(true);
    gl.clearDepth(1);
    gl.clear(gl.DEPTH_BUFFER_BIT);
    gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
    const gp = this.progs.grain;
    gl.useProgram(gp.p);
    bindTex(gl, gp, 'uA', sim.A, 0);
    bindTex(gl, gp, 'uB', sim.B, 1);
    bindTex(gl, gp, 'uG0', sim.G0, 2);
    bindTex(gl, gp, 'uScreen', this.screenTex, 3);
    bindTex(gl, gp, 'uShadow', this.shadowTex[0], 4);
    setUniforms(gl, gp, {
      ...common,
      uRes: [this.w, this.h],
      uMaxPt: this.maxPoint,
      uCull: look.cull,
      uSq: look.sq,
      uLook: [look.amb, look.key, look.sparkle, look.transl],
      uScreenGain: look.screenGain,
    });
    gl.drawArrays(gl.POINTS, 0, sim.active);
    gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
    gl.disable(gl.DEPTH_TEST);
  }
}