Rotierende Zylindertrommel mit Bildfeldern
Simuliert eine rotierende zylindrische Trommel mit mehreren Bildfeldern. Verwendet 3D-Projektion, Tiefensortierung und Lambert-Schattierung, um einen texturierten 3D-Effekt auf einer 2D-Leinwand zu erzeugen.
INPUT
OUTPUT
JavaScript
// Rotating cylinder drum with image panels
// demo_drum.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
// demo_drum.js — Rotating cylinder drum with N image panels, Lambertian shading, depth sorting.
// Each panel shows the full source image; Lambert shading differentiates them visually.
// Vector overlay draws rim circles only (no seam lines — they cut through the image).
const W = 800, H = 600;
// N=12 → 30°-wide panels read as a smooth cylinder; lower N (e.g. 5)
// makes each panel a wide chord that visibly cuts through the
// cylinder interior. R / H_PANEL / EYE z are tuned together so the
// front of the cylinder fits inside the canvas with a comfortable
// margin — a too-tall H_PANEL or a too-close camera clips the rim
// circles off the top and bottom.
const N = 12;
const R = 1.8;
const H_PANEL = 1.0;
const ROTATION_Y = -Math.PI * 0.04; // ~−7° — small offset so a panel sits near front-centre
const dTheta = (2 * Math.PI) / N;
// --- Camera & projection ---
const EYE = Matrix.Vec3.fromValues(0.0, 0.5, 5.5);
const CENTER = Matrix.Vec3.fromValues(0, 0, 0);
const UP = Matrix.Vec3.fromValues(0, 1, 0);
const view = new Matrix.Mat4().lookAt(EYE, CENTER, UP);
const proj = new Matrix.Mat4().perspectiveNO(Math.PI / 3.0, W / H, 0.1, 100);
const model = new Matrix.Mat4().fromYRotation(ROTATION_Y);
// MVP = proj * view * model
const mvp = Matrix.Mat4.clone(proj).multiply(view).multiply(model);
// --- Project a local-space point [x,y,z] to screen [sx, sy] ---
function project(v) {
const p = Matrix.Vec3.fromValues(v[0], v[1], v[2]).transformMat4(mvp);
return [
(p[0] + 1) / 2 * W,
(1 - (p[1] + 1) / 2) * H
];
}
// --- Return NDC Z of a local-space point ---
function ndcZOf(v) {
const p = Matrix.Vec3.fromValues(v[0], v[1], v[2]).transformMat4(mvp);
return p[2];
}
function clamp(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }
function normalise3(v) {
const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
v[0] /= len; v[1] /= len; v[2] /= len;
return v;
}
function dot3(a, b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }
// Sun direction = normalised camera position
const eyeArr = [EYE[0], EYE[1], EYE[2]];
const sunDir = normalise3([...eyeArr]);
// ============================================================
// Build panel descriptors
// ============================================================
const panels = [];
for (let i = 0; i < N; i++) {
const t0 = i * dTheta;
const t1 = (i + 1) * dTheta;
const midTheta = (t0 + t1) / 2;
const TL = [R * Math.cos(t0), +H_PANEL, R * Math.sin(t0)];
const TR = [R * Math.cos(t1), +H_PANEL, R * Math.sin(t1)];
const BR = [R * Math.cos(t1), -H_PANEL, R * Math.sin(t1)];
const BL = [R * Math.cos(t0), -H_PANEL, R * Math.sin(t0)];
const worldNormal = [
Math.cos(midTheta + ROTATION_Y),
0,
Math.sin(midTheta + ROTATION_Y)
];
const visible = worldNormal[2] * EYE[2] > 0;
// Lambertian shading — min 0.3 ambient so dark faces still read
const shade = clamp(0.30 + 0.70 * Math.max(0, dot3(worldNormal, sunDir)), 0.30, 1.0);
const depth = (ndcZOf(TL) + ndcZOf(TR) + ndcZOf(BR) + ndcZOf(BL)) / 4;
panels.push({ i, TL, TR, BR, BL, shade, visible, depth });
}
// Painter's sort — farthest first
const visiblePanels = panels
.filter(p => p.visible)
.sort((a, b) => b.depth - a.depth);
// ============================================================
// Main rendering
// ============================================================
const src = Engine.loadImage(INPUT);
// Fast solid background via 1×1 resize
const result = Engine.createImage(1, 1);
result.setPixel(px(0, 0), new Pixel(0.04, 0.04, 0.07, 1.0));
result.resize(W, H);
// --- Stamp visible panels back-to-front ---
// Panoramic wrap: resize source to N×height so each strip is square (srcH×srcH).
// Avoids the extreme aspect-ratio mismatch that caused ugly stretch on narrow strips.
const srcH = src.height;
const panoramic = src.clone().resize(srcH * N, srcH);
const sliceW = srcH; // each strip is exactly square
// Shift image mapping so the face (centre of source) falls on the front panels.
const PANEL_OFFSET = Math.floor(N / 2);
for (const p of visiblePanels) {
const rawCorners = [project(p.TL), project(p.TR), project(p.BR), project(p.BL)];
const imgIdx = (p.i + PANEL_OFFSET) % N;
const tile = panoramic.clone().crop(imgIdx * sliceW, 0, sliceW, srcH);
tile.brightness(p.shade);
result.stampAt(tile, rawCorners.map(([x,y]) => px(x,y)), 1.0, Blend.Over);
tile.free();
}
panoramic.free();
// ============================================================
// Vector overlay — rim circles only (no seam lines)
// ============================================================
const RIM_STEPS = 80;
const wireCanvas = Engine.createCanvas(W, H);
function drawRim(y_level, alpha, lineWidth) {
wireCanvas.pen(new Pixel(1.0, 1.0, 1.0, alpha), lineWidth);
const path = Engine.createPath();
const first = project([R * Math.cos(0), y_level, R * Math.sin(0)]);
path.moveTo(first[0], first[1]);
for (let s = 1; s <= RIM_STEPS; s++) {
const angle = (s / RIM_STEPS) * 2 * Math.PI;
const pt = project([R * Math.cos(angle), y_level, R * Math.sin(angle)]);
path.lineTo(pt[0], pt[1]);
}
path.close();
wireCanvas.drawPath(path, true);
path.free();
}
drawRim(+H_PANEL, 0.50, 1.5);
drawRim(-H_PANEL, 0.50, 1.5);
const wireImg = wireCanvas.toImage();
result.blendAt(wireImg, px(0, 0), 1.0, Blend.Over);
wireImg.free();
Engine.saveImage(result, OUTPUT);
result.free();
src.free();
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0