start . me .
Directory path . web , canvas .

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>