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 };
});