start .
me .
HTML Document File : raymarching-voxel.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ray Marched Voxel Model with Textures</title>
<style>
/* body { margin: 0; overflow: hidden; background: #1a1a1a; display: flex; justify-content: center; align-items: center; height: 100vh; } */
canvas { border: 1px solid #333; }
</style>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
const data = imageData.data;
// Vector operations
const vec3 = (x, y, z) => ({ x, y, z });
const add = (a, b) => vec3(a.x + b.x, a.y + b.y, a.z + b.z);
const sub = (a, b) => vec3(a.x - b.x, a.y - b.y, a.z - b.z);
const mul = (v, s) => vec3(v.x * s, v.y * s, v.z * s);
const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
const normalize = (v) => {
const len = Math.sqrt(dot(v, v));
return mul(v, 1 / len);
};
const reflect = (v, n) => sub(v, mul(n, 2 * dot(v, n)));
const cross = (a, b) => vec3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
);
// Scene objects
const spheres = [
{ pos: vec3(0, 1, 0), radius: 1 },
{ pos: vec3(2, 0.5, 2), radius: 0.5 },
];
const boxes = [
{ pos: vec3(-2, 1, -1), size: vec3(0.8, 0.8, 0.8) },
];
// Distance functions with textures
const sdSphere = (p, s) => Math.sqrt(dot(p, p)) - s.radius;
const sdBox = (p, b) => {
const q = vec3(Math.abs(p.x) - b.size.x, Math.abs(p.y) - b.size.y, Math.abs(p.z) - b.size.z);
const exterior = Math.sqrt(Math.max(q.x, 0) ** 2 + Math.max(q.y, 0) ** 2 + Math.max(q.z, 0) ** 2);
const interior = Math.min(Math.max(q.x, Math.max(q.y, q.z)), 0);
return exterior + interior;
};
const sdFloor = (p) => {
const scale = 2.0;
const checker = (Math.floor(p.x * scale) + Math.floor(p.z * scale)) % 2;
return {
dist: p.y,
color: checker === 0 ? vec3(0.3, 0.3, 0.3) : vec3(0.7, 0.7, 0.7)
};
};
const sdVoxelPyramid = (p) => {
const layerHeight = 0.3;
const layerSizeReduction = 0.2;
let minDist = Infinity;
let color = vec3(0.3, 0.3, 0.7); // Initial color
for (let i = 0; i < 5; i++) {
const layerPos = vec3(0, i * layerHeight, 0);
const layerSize = 1 - i * layerSizeReduction;
const boxPos = sub(p, layerPos);
const boxDist = sdBox(boxPos, { size: vec3(layerSize, layerHeight, layerSize) });
if (boxDist < minDist) {
minDist = boxDist;
color = vec3(0.3 + 0.1 * i, 0.3, 0.7); // Color gradient by layer
}
}
return { dist: minDist, color: color };
};
const map = (p) => {
let { dist: minDist, color } = sdFloor(p);
for (let sphere of spheres) {
const sphereP = sub(p, sphere.pos);
const dist = sdSphere(sphereP, sphere);
if (dist < minDist) {
minDist = dist;
color = vec3(1.0, 0.5, 0.5); // Color for spheres
}
}
for (let box of boxes) {
const boxP = sub(p, box.pos);
const dist = sdBox(boxP, box);
if (dist < minDist) {
minDist = dist;
color = vec3(0.5, 1.0, 0.5); // Color for boxes
}
}
const pyramidDist = sdVoxelPyramid(sub(p, vec3(0, 0, -2)));
if (pyramidDist.dist < minDist) {
minDist = pyramidDist.dist;
color = pyramidDist.color;
}
return { dist: minDist, color: color };
};
const calcNormal = (p) => {
const e = 0.001;
const n = vec3(
map(vec3(p.x + e, p.y, p.z)).dist - map(vec3(p.x - e, p.y, p.z)).dist,
map(vec3(p.x, p.y + e, p.z)).dist - map(vec3(p.x, p.y - e, p.z)).dist,
map(vec3(p.x, p.y, p.z + e)).dist - map(vec3(p.x, p.y, p.z - e)).dist
);
return normalize(n);
};
const march = (ro, rd, maxSteps = 100, maxDist = 100, minDist = 0.01) => {
let t = 0;
let color = vec3(0.0, 0.0, 0.0);
for (let i = 0; i < maxSteps; i++) {
const p = add(ro, mul(rd, t));
const result = map(p);
if (result.dist < minDist) {
return { hit: true, dist: t, pos: p, color: result.color };
}
if (t > maxDist) break;
t += result.dist;
}
return { hit: false, color: vec3(0.2, 0.3, 0.5) }; // Background color
};
const render = () => {
const time = Date.now() * 0.001;
const cameraPos = vec3(Math.sin(time * 0.5) * 8, 6, Math.cos(time * 0.5) * 8);
const lookAt = vec3(0, 0, 0);
const up = vec3(0, 1, 0);
const forward = normalize(sub(lookAt, cameraPos));
const right = normalize(cross(forward, up));
const realUp = normalize(cross(right, forward));
const perspectiveScale = 2.0;
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const fx = (x / canvas.width) * 2 - 1;
const fy = (y / canvas.height) * 2 - 1;
const rd = normalize(add(add(mul(forward, perspectiveScale), mul(right, fx)), mul(realUp, -fy)));
let color = vec3(0, 0, 0);
let contribution = 1;
let ro = cameraPos;
let rayDir = rd;
for (let bounce = 0; bounce < 3; bounce++) {
const result = march(ro, rayDir);
if (result.hit) {
const normal = calcNormal(result.pos);
const lightDir = normalize(vec3(1, 1, -1));
const diff = Math.max(0, dot(normal, lightDir));
color = add(color, mul(result.color, diff * contribution));
ro = add(result.pos, mul(normal, 0.01));
rayDir = reflect(rayDir, normal);
contribution *= 0.7;
} else {
color = add(color, vec3(0.2, 0.3, 0.5)); // Background color
break;
}
}
const i = (y * canvas.width + x) * 4;
data[i] = Math.min(255, color.x * 255);
data[i + 1] = Math.min(255, color.y * 255);
data[i + 2] = Math.min(255, color.z * 255);
data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
requestAnimationFrame(render);
};
render();
</script>
<hr>
<a href=".">go to current directory contents</a>
<hr>
<script src="https://arkenidar.com/web/show-source.js"></script>
</body>
</html>