<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
This Pen doesn't use any external CSS resources.