<h4>Ray Tracing in One Weekend</h4>
参考:https://raytracing.github.io/<br>
FPS: <span id="fps"></span><br>
const vertexShaderSource = `
attribute vec4 a_Position;
void main() {
    gl_Position = a_Position;
}
`;

const fragmentShaderSource = `
precision highp float;
uniform float u_Time;
uniform vec2 u_Resolution;

const int samples_per_pixel = 40;
const float PI = 3.14159265359;
const int MAX_DEPTH = 10;

const int MATERIAL_LAMBERTIAN = 1; // 拡散
const int MATERIAL_METAL = 2; // 金属
const int MATERIAL_DIELECTRIC = 3; // ガラス

// ------------------------------------------------------------------------------------------------ Ray

struct Ray {
    vec3 orig;
    vec3 dir;
};

vec3 Ray_at(Ray r, float t) {
    return r.orig + t * r.dir;
}

struct Material {
    int type;
    vec3 albedo;
    float fuzz;
    float ref_idx;
};

struct HitRecord {
    vec3 p;
    vec3 normal;
    float t;
    bool front_face;
    Material material;
};

void HitRecord_setFaceNormal(inout HitRecord rec, Ray r, vec3 outward_normal) {
    rec.front_face = dot(r.dir, outward_normal) < 0.0;
    rec.normal = rec.front_face ? outward_normal : -outward_normal;
}

// ------------------------------------------------------------------------------------------------ utils

float rndNum;

// 0.0~1.0を返す疑似乱数生成
float random() {
    vec3 seed = vec3(gl_FragCoord.xy, rndNum);
    rndNum = rndNum + 1.0;
    float dt = dot(seed.xy, vec2(12.9898, 78.233)) + seed.z;
    return fract(sin(dt) * 43758.5453);
}

// [min,max) の実数乱数を返す
float random_range(float min, float max) {
    return min + (max - min) * random();
}

vec3 random_vec3() {
    return vec3(random(), random(), random());
}

vec3 random_vec3_range(float min, float max) {
    return vec3(random_range(min, max), random_range(min, max), random_range(min, max));
}

vec3 random_in_unit_sphere() {
    for (int i = 0; i < 100; i++) {
        vec3 p = random_vec3_range(-1.0, 1.0);
        if (dot(p, p) < 1.0) {
            return p;
        }
    }
    return vec3(0.0); // 諦める
}

vec3 random_unit_vector() {
    float a = random_range(0.0, 2.0 * PI);
    float z = random_range(-1.0, 1.0);
    float r = sqrt(1.0 - z * z);
    return vec3(r * cos(a), r * sin(a), z);
}

vec3 random_in_unit_disk() {
    for (int i = 0; i < 100; i++) {
        vec3 p = vec3(random_range(-1.0, 1.0), random_range(-1.0, 1.0), 0);
        if (dot(p, p) < 1.0) {
            return p;
        }
    }
    return vec3(0.0); // 諦める
}

float degrees_to_radians(float degrees) {
    return degrees * PI / 180.0;
}

// ------------------------------------------------------------------------------------------------ マテリアル

Material Material_initLambertian(vec3 albedo) {
    return Material(MATERIAL_LAMBERTIAN, albedo, 0.0, 0.0);
}

Material Material_initMetal(vec3 albedo, float fuzz) {
    return Material(MATERIAL_METAL, albedo, fuzz, 0.0);
}

Material Material_initDielectric(float ref_idx) {
    return Material(MATERIAL_DIELECTRIC, vec3(0.0), 0.0, ref_idx);
}

float schlick(float cosine, float ref_idx) {
    float r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
    r0 = r0 * r0;
    return r0 + (1.0 - r0) * pow((1.0 - cosine), 5.0);
}

bool Material_scatter(Material mat, Ray r_in, HitRecord rec, inout vec3 attenuation, inout Ray scattered) {
    if (mat.type == MATERIAL_METAL) {
        vec3 reflected = reflect(normalize(r_in.dir), rec.normal);
        scattered = Ray(rec.p, reflected + mat.fuzz * random_in_unit_sphere());
        attenuation = mat.albedo;
        return (dot(scattered.dir, rec.normal) > 0.0);
    } else if (mat.type == MATERIAL_LAMBERTIAN) {
        vec3 scatter_direction = rec.normal + random_unit_vector();
        scattered = Ray(rec.p, scatter_direction);
        attenuation = mat.albedo;
        return true;
    } else if (mat.type == MATERIAL_DIELECTRIC) {
        attenuation = vec3(1.0, 1.0, 1.0);
        float etai_over_etat = (rec.front_face) ? (1.0 / mat.ref_idx) : (mat.ref_idx);

        vec3 unit_direction = normalize(r_in.dir);
        float cos_theta = min(dot(-unit_direction, rec.normal), 1.0);
        float sin_theta = sqrt(1.0 - cos_theta * cos_theta);
        if (etai_over_etat * sin_theta > 1.0) {
            vec3 reflected = reflect(unit_direction, rec.normal);
            scattered = Ray(rec.p, reflected);
            return true;
        }
        float reflect_prob = schlick(cos_theta, etai_over_etat);
        if (random() < reflect_prob) {
            vec3 reflected = reflect(unit_direction, rec.normal);
            scattered = Ray(rec.p, reflected);
            return true;
        }
        vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat);
        scattered = Ray(rec.p, refracted);
        return true;
    }
}

// ------------------------------------------------------------------------------------------------ 球体

const int SPHERES_SIZE = 4;
struct Sphere {
    vec3 center;
    float radius;
    Material material;
};

Sphere spheres[SPHERES_SIZE];

bool Sphere_hit(Sphere sphere, Ray r, float ray_tmin, float ray_tmax, inout HitRecord rec) {
    vec3 oc = sphere.center - r.orig;
    float a = dot(r.dir, r.dir);
    float h = dot(r.dir, oc);
    float c = dot(oc, oc) - sphere.radius * sphere.radius;
    float discriminant = h * h - a * c;

    if (discriminant < 0.0) {
        return false;
    }

    float sqrtd = sqrt(discriminant);

    // Find the nearest root that lies in the acceptable range.
    float root = (h - sqrtd) / a;
    if (root <= ray_tmin || ray_tmax <= root) {
        root = (h + sqrtd) / a;
        if (root <= ray_tmin || ray_tmax <= root) {
            return false;
        }
    }

    rec.t = root;
    rec.p = Ray_at(r, rec.t);
    rec.material = sphere.material;
    vec3 outward_normal = (rec.p - sphere.center) / sphere.radius;
    HitRecord_setFaceNormal(rec, r, outward_normal);

    return true;
}

bool spheresHit(Ray r, float ray_tmin, float ray_tmax, inout HitRecord rec) {
    HitRecord temp_rec;
    bool hit_anything = false;
    float closest_so_far = ray_tmax;

    for (int i = 0; i < SPHERES_SIZE; i++) {
        if (Sphere_hit(spheres[i], r, ray_tmin, closest_so_far, temp_rec)) {
            hit_anything = true;
            closest_so_far = temp_rec.t;
            rec = temp_rec;
        }
    }

    return hit_anything;
}

// ------------------------------------------------------------------------------------------------ camera

struct Camera {
    vec3 origin;
    vec3 lower_left_corner;
    vec3 horizontal;
    vec3 vertical;
    vec3 u, v, w;
    float lens_radius;
};

void Camera_init(inout Camera camera, vec3 lookfrom, vec3 lookat, vec3 vup, float vfov, float aspect_ratio, float aperture, float focus_dist) {
    float theta = degrees_to_radians(vfov);
    float h = tan(theta / 2.0);
    float viewport_height = 2.0 * h;
    float viewport_width = aspect_ratio * viewport_height;

    camera.w = normalize(lookfrom - lookat);
    camera.u = normalize(cross(vup, camera.w));
    camera.v = cross(camera.w, camera.u);

    camera.origin = lookfrom;
    camera.horizontal = focus_dist * viewport_width * camera.u;
    camera.vertical = focus_dist * viewport_height * camera.v;
    camera.lower_left_corner = camera.origin - camera.horizontal / 2.0 - camera.vertical / 2.0 - focus_dist * camera.w;
    camera.lens_radius = aperture / 2.0;
}

Ray Camera_getRay(inout Camera camera, float u, float v) {
    vec3 rd = camera.lens_radius * random_in_unit_disk();
    vec3 offset = u * rd.xxx + v * rd.yyy;

    return Ray(camera.origin + offset, camera.lower_left_corner + u * camera.horizontal + v * camera.vertical - camera.origin - offset);
}

// ------------------------------------------------------------------------------------------------ render

vec3 ray_color(Ray r) {
    vec3 accumulated_color = vec3(1.0, 1.0, 1.0);

    for (int depth = 0; depth < MAX_DEPTH; depth++) {
        HitRecord rec;

        // レイがオブジェクトにヒットしたかを確認
        if (spheresHit(r, 0.001, 3.402823466e+38, rec)) {
            Ray scattered;
            vec3 attenuation;
            if (!Material_scatter(rec.material, r, rec, attenuation, scattered)) {
                return vec3(0.0);
            }
            r = scattered;
            accumulated_color *= attenuation;
        } else {
            // 背景色を計算
            vec3 unit_direction = normalize(r.dir);
            float a = 0.5 * (unit_direction.y + 1.0);
            accumulated_color *= mix(vec3(1.0, 1.0, 1.0), vec3(0.5, 0.7, 1.0), a);
            return accumulated_color;
        }
    }

    return vec3(0.0);
}

// ------------------------------------------------------------------------------------------------ main

void main() {
    rndNum = 0.0;

    spheres[0] = Sphere(vec3(0.0, 0.0, 0.0), 0.5, Material_initLambertian(vec3(0.7, 0.3, 0.3)));
    spheres[1] = Sphere(vec3(0.0, -100.5, 0.0), 100.0, Material_initLambertian(vec3(0.8, 0.8, 0.0))); // big

    float a = -u_Time / 600.0;
    float dis = cos(u_Time / 1200.0) * 0.3 + 1.3;
    spheres[2] = Sphere(vec3(cos(a) * dis, 0.0, sin(a) * dis), 0.5, Material_initMetal(vec3(.8, .8, .8), 0.0));
    a += PI;
    spheres[3] = Sphere(vec3(cos(a) * dis, 0.0, sin(a) * dis), 0.5, Material_initDielectric(1.5));

    Camera cam;
    vec3 lookfrom = vec3(3.0, 2.0 + sin(u_Time / 1300.0) * 2.0, 3.0);
    vec3 lookat = vec3(0.0, 0.0, 0.0);
    vec3 vup = vec3(0.0, 1.0, 0.0);
    float dist_to_focus = length(lookfrom - lookat);
    float aperture = 0.1;

    Camera_init(cam, lookfrom, lookat, vup, 20.0, u_Resolution.x / u_Resolution.y, aperture, dist_to_focus);

    vec3 pixel_color = vec3(0.0, 0.0, 0.0);

    for (int i = 0; i < samples_per_pixel; i++) {
        float u = (gl_FragCoord.x + random() - 0.5) / (u_Resolution.x - 1.0);
        float v = (gl_FragCoord.y + random() - 0.5) / (u_Resolution.y - 1.0);
        Ray r = Camera_getRay(cam, u, v);
        pixel_color += ray_color(r);
    }

    float scale = 1.0 / float(samples_per_pixel);
    gl_FragColor = vec4(sqrt(scale * pixel_color.r), sqrt(scale * pixel_color.g), sqrt(scale * pixel_color.b), 1.0);
    //gl_FragColor = vec4(gl_FragCoord.x / u_Resolution.x, random(), random(), 1.0);
}
`;

$(async () => {
    const aspect_ratio = 16.0 / 9.0;
    const image_width = 400;
    let image_height = Math.floor(image_width / aspect_ratio);
    image_height = (image_height < 1) ? 1 : image_height;

    const canvas = document.createElement("canvas") as HTMLCanvasElement;
    canvas.width = image_width;
    canvas.height = image_height;
    document.body.appendChild(canvas);
    const canvasWebGL = new CanvasWebGL(canvas, vertexShaderSource, fragmentShaderSource);

    let fps = 0;
    let lastSection = 0;
    const onFrame = (time: DOMHighResTimeStamp) => {
        const section = Math.floor(time / 1000);
        if (lastSection != section) {
            lastSection = section;
            $("#fps").text(fps);
            fps = 0;
        }
        fps++;
        canvasWebGL.render(time);
        requestAnimationFrame(onFrame);
    };
    requestAnimationFrame(onFrame);
});

class CanvasWebGL {
    private readonly gl: WebGLRenderingContext;
    private readonly uTimeLocation: WebGLUniformLocation | null;

    constructor(canvas: HTMLCanvasElement, vertexShaderSource: string, fragmentShaderSource: string) {
        const gl = canvas.getContext("webgl");
        if (!gl) {
            throw "WebGL is not supported!";
        }
        this.gl = gl;

        const vertexShader = this.createShader(gl.VERTEX_SHADER, vertexShaderSource);
        const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragmentShaderSource);

        const program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);

        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            console.error('Program link error:', gl.getProgramInfoLog(program));
        }

        gl.useProgram(program);

        const vertices = new Float32Array([
            -1.0, -1.0,
            1.0, -1.0,
            -1.0, 1.0,
            1.0, 1.0
        ]);

        const buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

        const aPosition = gl.getAttribLocation(program, "a_Position");
        gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(aPosition);

        const uResolutionLocation = gl.getUniformLocation(program, "u_Resolution");
        gl.uniform2f(uResolutionLocation, canvas.width, canvas.height);

        this.uTimeLocation = gl.getUniformLocation(program, "u_Time");

        //this.render(0);
    }

    render(time: number) {
        const gl = this.gl;
        gl.uniform1f(this.uTimeLocation, time);
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

    private createShader(type: GLenum, source: string): WebGLShader {
        const shader = this.gl.createShader(type);
        if (!shader) {
            throw "shader can't created!"
        }
        this.gl.shaderSource(shader, source);
        this.gl.compileShader(shader);
        if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
            throw `Shader compile error: ${this.gl.getShaderInfoLog(shader)}`;
        }
        return shader;
    }
}
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js