← Natura·Docs/Modules/Source

creatures/skinned-gl.js

116 lines · in natura-src-1.0.0.zip as natura/creatures/skinned-gl.js · a module · its entry in the module reference

// creatures/skinned-gl.js -- a sculpted, skinned, TEXTURED creature in real time (2026-09-26: my zebra and giraffe).
//
// The canvas painter (render3d.js) shades a triangle soup, one colour per face: fine for creatures built from
// primitives, but a coat of crisp stripes or a thin cream net needs its texture. This draws a natura quad3d creature
// with WebGL: its full runtime skin, its baked coat, skinned on the GPU by its own rig (4 bones a vertex), the joint
// matrices blended from frames baked off its IK gait (quad3d/export_natura3d.py). Same projection and light as
// render3d's render() -- orthographic, turned by azimuth and elevation, the key from the upper front left -- so it
// sits in the bestiary as one of the family. A ground shadow is the same skin, flattened.
//
//   const R = SkinnedGL.create(canvas)        (a WebGL canvas, usually offscreen)
//   R.draw(D, { clip:'walk', t, az, el, scale, ox, oy, yc, bg:[r,g,b] })   -> draws; the canvas can then be blitted
// D = window.NATURA3D[name] (base64 typed arrays + frames + the coat as a data URI).
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.SkinnedGL = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  const VS = `
    attribute vec3 aP; attribute vec3 aN; attribute vec2 aUV; attribute vec4 aI; attribute vec4 aW;
    uniform vec4 uJ[96];            // 32 joints x 3 rows of a 3x4
    uniform vec2 uRot[2];           // (cos az, sin az), (cos el, sin el)
    uniform vec4 uView;             // scale/W*2, scale/H*2, (ox/W*2-1), (1-oy/H*2)
    uniform float uYc, uFlat;
    varying vec3 vN; varying vec2 vUV;
    vec3 xf(int j, vec4 p) { return vec3(dot(uJ[j*3], p), dot(uJ[j*3+1], p), dot(uJ[j*3+2], p)); }
    void main() {
      vec4 p = vec4(aP, 1.0), n = vec4(aN, 0.0);
      vec3 P = vec3(0.0), N = vec3(0.0);
      for (int k = 0; k < 4; k++) {
        float w = k == 0 ? aW.x : k == 1 ? aW.y : k == 2 ? aW.z : aW.w;
        int j = int(k == 0 ? aI.x : k == 1 ? aI.y : k == 2 ? aI.z : aI.w);
        if (w > 0.0) { P += w * xf(j, p); N += w * xf(j, n); }
      }
      if (uFlat > 0.5) P.y = -0.03;                                           // the ground shadow: the skin, flattened
      float ca = uRot[0].x, sa = uRot[0].y, ce = uRot[1].x, se = uRot[1].y;
      vec3 R = vec3(P.x * ca + P.z * sa, 0.0, -P.x * sa + P.z * ca);
      float y = P.y * ce - R.z * se, z = P.y * se + R.z * ce;
      vec3 Nr = vec3(N.x * ca + N.z * sa, 0.0, -N.x * sa + N.z * ca);
      vN = normalize(vec3(Nr.x, N.y * ce - Nr.z * se, N.y * se + Nr.z * ce));
      vUV = aUV;
      gl_Position = vec4(R.x * uView.x + uView.z, (y - uYc) * uView.y + uView.w, -z * 0.1, 1.0);
    }`;
  const FS = `
    precision highp float;                       // uFlat is shared with the vertex shader: the precisions must match
    varying vec3 vN; varying vec2 vUV;
    uniform sampler2D uTex; uniform float uFlat;
    void main() {
      if (uFlat > 0.5) { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.22); return; }
      vec3 L = normalize(vec3(-0.4, 0.8, 0.5));
      vec3 n = normalize(vN);
      float sh = min(1.0, 0.42 + 0.72 * max(0.0, dot(n, L)) + max(0.0, n.y) * 0.12);    // render3d's shade
      vec3 c = texture2D(uTex, vUV).rgb;
      gl_FragColor = vec4(c * sh, 1.0);
    }`;
  function b64(s, T) { const bin = atob(s), u = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i); return new T(u.buffer); }
  function create(canvas) {
    const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true, antialias: true, alpha: false });
    if (!gl) return null;
    const ext = gl.getExtension('OES_element_index_uint');
    function sh(type, src) { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
      if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; }
    const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FS));
    gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
    const loc = {}; ['aP', 'aN', 'aUV', 'aI', 'aW'].forEach((a) => loc[a] = gl.getAttribLocation(prog, a));
    ['uJ', 'uRot', 'uView', 'uYc', 'uFlat', 'uTex'].forEach((u) => loc[u] = gl.getUniformLocation(prog, u));
    const cache = new Map();
    function upload(D) {
      if (cache.has(D)) return cache.get(D);
      const G = D.gl, buf = (arr) => { const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, arr, gl.STATIC_DRAW); return b; };
      const m = { P: buf(b64(G.P, Float32Array)), N: buf(b64(G.N, Float32Array)), UV: buf(b64(G.UV, Float32Array)),
                  I: buf(new Float32Array(b64(G.I, Uint8Array))), W: buf(b64(G.W, Float32Array)) };
      const idx = b64(G.T, Uint32Array); m.count = idx.length;
      m.ib = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, m.ib); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx, gl.STATIC_DRAW);
      m.tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, m.tex);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([200, 190, 170, 255]));
      const img = new Image(); img.onload = () => { gl.bindTexture(gl.TEXTURE_2D, m.tex); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.generateMipmap(gl.TEXTURE_2D);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); m.ready = true;
        if (R.onTexture) R.onTexture(D); }; img.src = G.tex;          // (a page drawing once -- a static grid -- redraws)
      cache.set(D, m); return m;
    }
    function joints(D, clip, t) {
      const F = D.frames[clip], cyc = D.cycle[clip];
      const u = ((t / cyc) % 1 + 1) % 1, fx = u * F.length, i0 = Math.floor(fx) % F.length, i1 = (i0 + 1) % F.length, a = fx - Math.floor(fx);
      const J = new Float32Array(96 * 4);
      for (let j = 0; j < F[0].length && j < 32; j++) for (let k = 0; k < 12; k++) J[j * 12 + k] = F[i0][j][k] * (1 - a) + F[i1][j][k] * a;
      return J;
    }
    function draw(D, o) {
      const m = upload(D), W = canvas.width, H = canvas.height, bg = o.bg || [14, 17, 22];
      gl.viewport(0, 0, W, H);
      if (!o.keep) { gl.clearColor(bg[0] / 255, bg[1] / 255, bg[2] / 255, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }
      else gl.clear(gl.DEPTH_BUFFER_BIT);
      gl.useProgram(prog);
      const bind = (b, l, n) => { gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l, n, gl.FLOAT, false, 0, 0); };
      bind(m.P, loc.aP, 3); bind(m.N, loc.aN, 3); bind(m.UV, loc.aUV, 2); bind(m.I, loc.aI, 4); bind(m.W, loc.aW, 4);
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, m.ib);
      gl.uniform4fv(loc.uJ, joints(D, o.clip || 'walk', o.t || 0));
      gl.uniform2fv(loc.uRot, [Math.cos(o.az || 0), Math.sin(o.az || 0), Math.cos(o.el || 0), Math.sin(o.el || 0)]);
      gl.uniform4fv(loc.uView, [o.scale / W * 2, o.scale / H * 2, o.ox / W * 2 - 1, 1 - o.oy / H * 2]);
      gl.uniform1f(loc.uYc, o.yc || 0);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, m.tex); gl.uniform1i(loc.uTex, 0);
      const type = ext ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
      if (o.shadow !== false) {                                   // the shadow first, blended, no depth
        gl.disable(gl.DEPTH_TEST); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.uniform1f(loc.uFlat, 1); gl.drawElements(gl.TRIANGLES, m.count, type, 0); gl.disable(gl.BLEND);
      }
      gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK);
      gl.uniform1f(loc.uFlat, 0); gl.drawElements(gl.TRIANGLES, m.count, type, 0);
      return m.ready;
    }
    const R = { draw, gl, onTexture: null };
    return R;
  }
  return { create };
});