const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
let fs;
async function initFS() {
if (isNode) {
const fsModule = await import('fs');
fs = fsModule.default;
}
}
async function renderCube() {
let loadLocal =
isNode || (
typeof location !== 'undefined' &&
typeof location?.hostname === 'string' &&
location.hostname.toString() === 'localhost'
);
const { webGL2 } = await import(
loadLocal ? './index.js' :
'https://esm.run/webgl2'
);
const gl = await webGL2();
gl.verbosity = 0; gl.viewport(0, 0, 640, 480);
const vsSource = `#version 300 es
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 uv;
uniform mat4 u_mvp;
out vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = u_mvp * vec4(position, 1.0);
}
`;
const fsSource = `#version 300 es
precision highp float;
uniform texture2D u_texture;
uniform sampler u_sampler;
in vec2 v_uv;
out vec4 fragColor;
void main() {
fragColor = texture(sampler2D(u_texture, u_sampler), v_uv);
// fragColor = vec4(v_uv, 0.0, 1.0);
}
`;
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vsSource);
gl.compileShader(vs);
const fsShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fsShader, fsSource);
gl.compileShader(fsShader);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fsShader);
gl.linkProgram(program);
gl.useProgram(program);
const vertices = new Float32Array([
-0.5, -0.5, 0.5, 0.0, 0.0,
0.5, -0.5, 0.5, 1.0, 0.0,
0.5, 0.5, 0.5, 1.0, 1.0,
-0.5, -0.5, 0.5, 0.0, 0.0,
0.5, 0.5, 0.5, 1.0, 1.0,
-0.5, 0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
-0.5, 0.5, -0.5, 0.0, 1.0,
0.5, 0.5, -0.5, 1.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
0.5, 0.5, -0.5, 1.0, 1.0,
0.5, -0.5, -0.5, 1.0, 0.0,
-0.5, 0.5, -0.5, 0.0, 0.0,
-0.5, 0.5, 0.5, 0.0, 1.0,
0.5, 0.5, 0.5, 1.0, 1.0,
-0.5, 0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 0.5, 1.0, 1.0,
0.5, 0.5, -0.5, 1.0, 0.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
0.5, -0.5, -0.5, 1.0, 0.0,
0.5, -0.5, 0.5, 1.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
0.5, -0.5, 0.5, 1.0, 1.0,
-0.5, -0.5, 0.5, 0.0, 1.0,
0.5, -0.5, -0.5, 0.0, 0.0,
0.5, 0.5, -0.5, 0.0, 1.0,
0.5, 0.5, 0.5, 1.0, 1.0,
0.5, -0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 0.5, 1.0, 0.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
-0.5, -0.5, 0.5, 1.0, 0.0,
-0.5, 0.5, 0.5, 1.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
-0.5, 0.5, 0.5, 1.0, 1.0,
-0.5, 0.5, -0.5, 0.0, 1.0,
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 20, 0);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 12);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
const texData = new Uint8Array(16 * 16 * 4);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const idx = (y * 16 + x) * 4;
const isCheck = ((x >> 2) ^ (y >> 2)) & 1;
if (isCheck) {
texData[idx] = 255; texData[idx+1] = 215; texData[idx+2] = 0; texData[idx+3] = 255; } else {
texData[idx] = 100; texData[idx+1] = 149; texData[idx+2] = 237; texData[idx+3] = 255; }
}
}
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 16, 16, 0, gl.RGBA, gl.UNSIGNED_BYTE, texData);
const uTextureLoc = gl.getUniformLocation(program, "u_texture");
gl.uniform1i(uTextureLoc, 0);
const uSamplerLoc = gl.getUniformLocation(program, "u_sampler");
gl.uniform1i(uSamplerLoc, 0);
function perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
return [
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) * nf, -1,
0, 0, (2 * far * near) * nf, 0
];
}
function multiply(a, b) {
const out = new Float32Array(16);
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
let sum = 0;
for (let k = 0; k < 4; k++) {
sum += a[k * 4 + row] * b[col * 4 + k];
}
out[col * 4 + row] = sum;
}
}
return out;
}
function rotateY(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const r = [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
return multiply(m, r);
}
function rotateX(m, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const r = [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
return multiply(m, r);
}
function translate(m, x, y, z) {
const t = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1
];
return multiply(m, t);
}
let mvp = perspective(Math.PI / 4, 640 / 480, 0.1, 100.0);
mvp = translate(mvp, 0, 0, -3);
mvp = rotateX(mvp, 0.5);
mvp = rotateY(mvp, 0.8);
const mvpLoc = gl.getUniformLocation(program, "u_mvp");
gl.uniformMatrix4fv(mvpLoc, false, mvp);
console.log("MVP Matrix:", mvp);
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 36);
const pixels = new Uint8Array(640 * 480 * 4);
gl.readPixels(0, 0, 640, 480, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return { pixels, width: 640, height: 480 };
}
function savePNG(width, height, pixels, filename) {
const signature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
function createChunk(type, data) {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const typeBuf = Buffer.from(type);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
return Buffer.concat([len, typeBuf, data, crc]);
}
const ihdrData = Buffer.alloc(13);
ihdrData.writeUInt32BE(width, 0);
ihdrData.writeUInt32BE(height, 4);
ihdrData[8] = 8; ihdrData[9] = 6; ihdrData[10] = 0; ihdrData[11] = 0; ihdrData[12] = 0; const ihdr = createChunk('IHDR', ihdrData);
const scanlineSize = width * 4 + 1;
const uncompressedData = Buffer.alloc(height * scanlineSize);
for (let y = 0; y < height; y++) {
const srcY = height - 1 - y; uncompressedData[y * scanlineSize] = 0; pixels.copy(uncompressedData, y * scanlineSize + 1, srcY * width * 4, (srcY + 1) * width * 4);
}
const zlibHeader = Buffer.from([0x78, 0x01]);
const blocks = [];
for (let i = 0; i < uncompressedData.length; i += 65535) {
const remaining = uncompressedData.length - i;
const blockSize = Math.min(remaining, 65535);
const isLast = remaining <= 65535;
const blockHeader = Buffer.alloc(5);
blockHeader[0] = isLast ? 1 : 0;
blockHeader.writeUInt16LE(blockSize, 1);
blockHeader.writeUInt16LE(~blockSize & 0xFFFF, 3);
blocks.push(blockHeader);
blocks.push(uncompressedData.slice(i, i + blockSize));
}
const adler = Buffer.alloc(4);
adler.writeUInt32BE(adler32(uncompressedData), 0);
const idatData = Buffer.concat([zlibHeader, ...blocks, adler]);
const idat = createChunk('IDAT', idatData);
const iend = createChunk('IEND', Buffer.alloc(0));
fs.writeFileSync(filename, Buffer.concat([signature, ihdr, idat, iend]));
}
const crcTable = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
crcTable[i] = c;
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < buf.length; i++) {
crc = crcTable[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
function adler32(buf) {
let s1 = 1, s2 = 0;
for (let i = 0; i < buf.length; i++) {
s1 = (s1 + buf[i]) % 65521;
s2 = (s2 + s1) % 65521;
}
return ((s2 << 16) | s1) >>> 0;
}
async function main() {
await initFS();
const result = await renderCube();
const { pixels, width, height } = result;
if (isNode) {
savePNG(width, height, Buffer.from(pixels), 'output.png');
console.log("Saved output.png");
} else {
const style = document.createElement('style');
style.textContent = `
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #222;
color: #fff;
font-family: monospace;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
gap: 20px;
}
h1 {
font-size: 24px;
font-weight: normal;
}
canvas {
border: 2px solid #fff;
background: #000;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
`;
document.head.appendChild(style);
const h1 = document.createElement('h1');
h1.textContent = 'WebGL2 Polymorphic Cube Renderer';
document.body.appendChild(h1);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
const flipped = new Uint8ClampedArray(pixels.length);
for (let y = 0; y < height; y++) {
const srcY = height - 1 - y;
const srcOffset = srcY * width * 4;
const dstOffset = y * width * 4;
flipped.set(pixels.subarray(srcOffset, srcOffset + width * 4), dstOffset);
}
imageData.data.set(flipped);
ctx.putImageData(imageData, 0, 0);
console.log("Rendered cube to canvas");
}
}
main().catch(console.error);
export { renderCube, main };