周末光追的 ReScript 实现
直接编译运行即可
周末光追的 ReScript 实现
直接编译运行即可
| @inline | |
| let v3create = (a, b, c) => { | |
| [a, b, c] | |
| } | |
| @inline | |
| let v3x = (a) => Js.Array2.unsafe_get(a, 0) | |
| @inline | |
| let v3y = (a) => Js.Array2.unsafe_get(a, 1) | |
| @inline | |
| let v3z = (a) => Js.Array2.unsafe_get(a, 2) | |
| @inline | |
| let v3vneg = (a) => [-.v3x(a), -.v3y(a), -.v3z(a)] | |
| @inline | |
| let v3vadd = (a, b) => [v3x(a) +. v3x(b), v3y(a) +. v3y(b), v3z(a) +. v3z(b)] | |
| @inline | |
| let v3vsub = (a, b) => [v3x(a) -. v3x(b), v3y(a) -. v3y(b), v3z(a) -. v3z(b)] | |
| @inline | |
| let v3vmul = (a, b) => [v3x(a) *. v3x(b), v3y(a) *. v3y(b), v3z(a) *. v3z(b)] | |
| @inline | |
| let v3vdiv = (a, b) => [v3x(a) /. v3x(b), v3y(a) /. v3y(b), v3z(a) /. v3z(b)] | |
| @inline | |
| let v3mul = (a, t) => [v3x(a) *. t, v3y(a) *. t, v3z(a) *. t] | |
| @inline | |
| let v3div = (a, t) => [v3x(a) /. t, v3y(a) /. t, v3z(a) /. t] | |
| @inline | |
| let v3setx = (a, v) => Js.Array2.unsafe_set(a, 0, v) | |
| @inline | |
| let v3sety = (a, v) => Js.Array2.unsafe_set(a, 1, v) | |
| @inline | |
| let v3setz = (a, v) => Js.Array2.unsafe_set(a, 2, v) | |
| @inline | |
| let v3set = (a, b) => { Js.Array2.unsafe_set(a, 0, Js.Array2.unsafe_get(b, 0)); | |
| Js.Array2.unsafe_set(a, 1, Js.Array2.unsafe_get(b, 1)); | |
| Js.Array2.unsafe_set(a, 2, Js.Array2.unsafe_get(b, 2));} | |
| @inline | |
| let v3vnegInPlace = (a) => { v3setx(a, -.v3x(a)); | |
| v3sety(a, -.v3y(a)); | |
| v3setz(a, -.v3z(a)) } | |
| @inline | |
| let v3vaddInPlace = (a, b) => { v3setx(a, v3x(a) +. v3x(b)); | |
| v3sety(a, v3y(a) +. v3y(b)); | |
| v3setz(a, v3z(a) +. v3z(b))} | |
| @inline | |
| let v3vsubInPlace = (a, b) => { v3setx(a, v3x(a) -. v3x(b)); | |
| v3sety(a, v3y(a) -. v3y(b)); | |
| v3setz(a, v3z(a) -. v3z(b))} | |
| @inline | |
| let v3vmulInPlace = (a, b) => { v3setx(a, v3x(a) *. v3x(b)); | |
| v3sety(a, v3y(a) *. v3y(b)); | |
| v3setz(a, v3z(a) *. v3z(b))} | |
| @inline | |
| let v3vdivInPlace = (a, b) => { v3setx(a, v3x(a) /. v3x(b)); | |
| v3sety(a, v3y(a) /. v3y(b)); | |
| v3setz(a, v3z(a) /. v3z(b))} | |
| @inline | |
| let v3mulInPlace = (a, b) => { v3setx(a, v3x(a) *. b); | |
| v3sety(a, v3y(a) *. b); | |
| v3setz(a, v3z(a) *. b);} | |
| @inline | |
| let v3divInPlace = (a, b) => { v3setx(a, v3x(a) /. b); | |
| v3sety(a, v3y(a) /. b); | |
| v3setz(a, v3z(a) /. b);} | |
| @inline | |
| let v3len = (a) => { | |
| Js.Math.sqrt(v3x(a) *. v3x(a) +. v3y(a) *. v3y(a) +. v3z(a) *. v3z(a)) | |
| } | |
| @inline | |
| let v3lenS = (a) => v3x(a) *. v3x(a) +. v3y(a) *. v3y(a) +. v3z(a) *. v3z(a) | |
| @inline | |
| let v3unit = (a) => { | |
| let le = v3len(a) | |
| [v3x(a) /. le, v3y(a) /. le, v3z(a) /. le] | |
| } | |
| @inline | |
| let v3unitInPlace = (a) => { | |
| let le = v3len(a) | |
| v3setx(a, v3x(a) /. le) | |
| v3sety(a, v3y(a) /. le) | |
| v3setz(a, v3z(a) /. le) | |
| } | |
| @inline | |
| let v3dot = (a, b) => { | |
| v3x(a) *. v3x(b) +. v3y(a) *. v3y(b) +. v3z(a) *. v3z(b) | |
| } | |
| @inline | |
| let v3cross = (a, b) => { | |
| let r = v3y(a) *. v3z(b) -. v3z(a) *. v3y(b) | |
| let g = v3z(a) *. v3x(b) -. v3x(a) *. v3z(b) | |
| let b = v3x(a) *. v3y(b) -. v3y(a) *. v3x(b) | |
| [r, g, b] | |
| } | |
| @inline | |
| let v3nearZero = (a) => { | |
| let epsilon = 1e-8 | |
| let abs = Js.Math.abs_float | |
| abs(v3x(a)) < epsilon && abs(v3y(a)) < epsilon && abs(v3z(a)) < epsilon | |
| } | |
| type v3 = array<float> | |
| type fd | |
| @module("fs") | |
| external openSync: (string, string) => fd = "openSync" | |
| @module("fs") | |
| external closeSync: (fd) => () = "closeSync" | |
| @module("fs") | |
| external writeSync: (fd, string) => () = "writeSync" | |
| let newline = '\n'->String.make(1, _) | |
| let writeV3 = (file: fd, v: v3) => { | |
| let r = Belt.Float.toInt(v3x(v) *. 255.999)->Belt.Int.toString | |
| let g = Belt.Float.toInt(v3y(v) *. 255.999)->Belt.Int.toString | |
| let b = Belt.Float.toInt(v3z(v) *. 255.999)->Belt.Int.toString | |
| writeSync(file, r ++ " " ++ g ++ " " ++ b ++ newline) | |
| } | |
| let gen = (~width: int, ~height: int, file: string) => { | |
| let f = openSync(file, "w") | |
| let rest = ref(width * height) | |
| writeSync(f, "P3" ++ newline) | |
| writeSync(f, width->Belt.Int.toString ++ " " | |
| ++ height->Belt.Int.toString ++ newline) | |
| writeSync(f, "256" ++ newline) | |
| (. v: v3) => { | |
| if rest.contents == -1 { | |
| Js.log("already finish") | |
| } else { | |
| writeV3(f, v) | |
| rest := rest.contents - 1 | |
| if (rest.contents == 0) { | |
| closeSync(f) | |
| rest := -1 | |
| } | |
| } | |
| } | |
| } | |
| let writemat = (mat: array<array<v3>>, file: string) => { | |
| let out = gen(~width = mat[0]->Js.Array2.length, | |
| ~height = mat->Js.Array2.length, | |
| file) | |
| mat->Belt.Array.forEachU((. a) => { | |
| a->Belt.Array.forEachU((. v) => { | |
| out(. v) | |
| }) | |
| }) | |
| } | |
| type ray = { | |
| origin: v3, | |
| direction: v3 | |
| } | |
| @inline | |
| let rayorigin = (a: ray) => a.origin | |
| @inline | |
| let raydirection = (a: ray) => a.direction | |
| @inline | |
| let rayat = (a: ray, t: float) => v3vadd(rayorigin(a), raydirection(a)->v3mul(t)) | |
| @inline | |
| let pi = 3.1415926535897932385 | |
| @val external inf: float = "Infinity" | |
| @inline | |
| let d2r = (deg) => deg *. pi /. 180.0 | |
| let rand = Js.Math.random | |
| let abs = Js.Math.abs_float | |
| let min = Js.Math.min_float | |
| type rec hit_record = { | |
| p: v3, | |
| normal: v3, | |
| t: float, | |
| front: bool, | |
| mater: material | |
| } | |
| and material = (. ray, hit_record) => option<(v3, ray)> | |
| @inline | |
| let hit_set_normal = (ra, nor) => v3dot(ra->raydirection, nor) < 0.0 ? (true, nor) : (false, nor->v3vneg) | |
| type hittable = (. ray, float, float) => option<hit_record> | |
| let hitByList = (. ray, tmin, tmax, hit_arr: array<hittable>) => { | |
| hit_arr->Belt.Array.reduceU((tmax, None), (. c, a) => { | |
| let (curr_max, _) = c | |
| let res = a(. ray, tmin, curr_max) | |
| switch res { | |
| | None => c | |
| | Some(hi) => { | |
| (hi.t, res) | |
| } | |
| } | |
| })->((_, ret))=>ret | |
| } | |
| let sphere2hittable = (center: v3, rad: float, mat: material) => { | |
| let hit: hittable = (. r, tmin, tmax) => { | |
| let oc = r->rayorigin->v3vsub(center) | |
| let a = r->raydirection->v3lenS | |
| let hb = v3dot(oc, r->raydirection) | |
| let c = oc->v3lenS -. rad *. rad | |
| let de = hb *. hb -. a *. c | |
| de < 0.0 ? None : { | |
| let sqrtde = de->Js.Math.sqrt | |
| let root = (-.hb -. sqrtde) /. a | |
| if (root < tmin || tmax < root) { | |
| let root = (-.hb +. sqrtde) /. a | |
| if (root < tmin || tmax < root) { | |
| None | |
| } else { | |
| let p = r->rayat(root) | |
| let nor = p->v3vsub(center)->v3div(rad) | |
| let (front, normal) = hit_set_normal(r, nor) | |
| Some({t: root, | |
| p: p, | |
| normal: normal, | |
| front: front, | |
| mater: mat}) | |
| } | |
| } else { | |
| let p = r->rayat(root) | |
| let nor = p->v3vsub(center)->v3div(rad) | |
| let (front, normal) = hit_set_normal(r, nor) | |
| Some({t: root, | |
| p: p, | |
| normal: normal, | |
| front: front, | |
| mater: mat}) | |
| } | |
| } | |
| } | |
| hit | |
| } | |
| let random_in_unit_disk = () => { | |
| let res = ref(v3create(0.0, 0.0, 0.0)) | |
| let flag = ref(true) | |
| while flag.contents { | |
| let p = v3create(rand() *. 2.0 -. 1.0, rand() *. 2.0 -. 1.0, 0.0) | |
| if (p->v3lenS < 1.0) { | |
| res := p | |
| flag := false | |
| } | |
| } | |
| res.contents | |
| } | |
| type cam = { | |
| origin: v3, | |
| lower_left_corner: v3, | |
| horizontal: v3, | |
| vertical: v3, | |
| u: v3, | |
| v: v3, | |
| w: v3, | |
| lens_radius: float | |
| } | |
| let camcreate = (~lookfrom, | |
| ~lookat, | |
| ~vup, | |
| ~aspect_ratio, | |
| ~viewport_height as vh, | |
| ~vfov, | |
| ~aperture, | |
| ~focus_dist) => | |
| { | |
| let theta = d2r(vfov) | |
| let h = Js.Math.tan(theta /. 2.0) | |
| let viewport_height = vh *. h | |
| let viewport_width = aspect_ratio *. viewport_height | |
| let w = lookfrom->v3vsub(lookat)->v3unit | |
| let u = v3cross(vup, w)->v3unit | |
| let v = v3cross(w, u) | |
| let origin = lookfrom | |
| let horizontal = v3mul(u, viewport_width *. focus_dist) | |
| let vertical = v3mul(v, viewport_height *. focus_dist) | |
| let lower_left_corner = origin | |
| ->v3vsub(v3div(horizontal, 2.0)) | |
| ->v3vsub(v3div(vertical, 2.0)) | |
| ->v3vsub(v3mul(w, focus_dist)) | |
| { | |
| origin: origin, | |
| lower_left_corner: lower_left_corner, | |
| horizontal: horizontal, | |
| vertical: vertical, | |
| w: w, | |
| u: u, | |
| v: v, | |
| lens_radius: aperture /. 2.0 | |
| } | |
| } | |
| let camgetray = (. came: cam, s: float, t: float) => { | |
| let rd = (random_in_unit_disk())->v3mul(came.lens_radius) | |
| let offset = v3vadd(came.u->v3mul(v3x(rd)), came.v->v3mul(v3y(rd))) | |
| {origin: came.origin->v3vadd(offset), | |
| direction: came.lower_left_corner | |
| ->v3vadd(v3mul(came.horizontal, s)) | |
| ->v3vadd(v3mul(came.vertical, t)) | |
| ->v3vsub(came.origin) | |
| ->v3vsub(offset)} | |
| } | |
| @inline | |
| let clamp = (x: float, min: float, max: float) => { | |
| x < min ? min : x > max ? max : x | |
| } | |
| let random_in_unit_sphere = () => { | |
| let flag = ref(true) | |
| let r = ref(v3create(0.0, 0.0, 0.0)) | |
| while (flag.contents) { | |
| let r1 = v3create(rand() *. 2.0 -. 1.0, | |
| rand() *. 2.0 -. 1.0, | |
| rand() *. 2.0 -. 1.0) | |
| if (v3lenS(r1) <= 1.0) { | |
| flag := false | |
| r := r1 | |
| } | |
| } | |
| r.contents | |
| } | |
| let random_in_hemisphere = (normal) => { | |
| let ve = random_in_unit_sphere() | |
| v3dot(ve, normal) > 0.0 ? ve : v3vneg(ve) | |
| } | |
| let lambertian = (color) => { | |
| let ret: material = (. _, hitre) => { | |
| let scatter_direction = hitre.normal->v3vadd(v3unit(random_in_unit_sphere())) | |
| let dir = v3nearZero(scatter_direction) ? hitre.normal : scatter_direction | |
| let ra = {origin: hitre.p, direction: dir} | |
| Some((color, ra)) | |
| } | |
| ret | |
| } | |
| @inline | |
| let reflect = (v: v3, n: v3) => { | |
| v->v3vsub(n->v3mul(v3dot(v, n) *. 2.0)) | |
| } | |
| let metal = (color, fuzz) => { | |
| let f = fuzz < 0.0 ? 0.0 : fuzz < 1.0 ? fuzz : 1.0 | |
| let ret: material = (. ray, hitre) => { | |
| let reflected = reflect(ray->raydirection->v3unit, hitre.normal) | |
| let scattered = {origin: hitre.p, | |
| direction: reflected | |
| ->v3vadd(v3mul(random_in_unit_sphere(), f))} | |
| if (v3dot(scattered->raydirection, hitre.normal) > 0.0) { | |
| Some((color, scattered)) | |
| } else { | |
| None | |
| } | |
| } | |
| ret | |
| } | |
| @inline | |
| let refract = (uv, normal, etai_div_etat) => { | |
| let cos_theta = min(-.v3dot(uv, normal), 1.0) | |
| let r_out_perp = uv->v3vadd(v3mul(normal, cos_theta))->v3mul(etai_div_etat) | |
| let r_out_parallel = -.Js.Math.sqrt(abs(1.0 -. r_out_perp->v3lenS))->v3mul(normal, _) | |
| v3vadd(r_out_parallel, r_out_perp) | |
| } | |
| @inline | |
| let reflectance = (cosine, ref_idx) => { | |
| let r0 = (1.0 -. ref_idx) /. (1.0 +. ref_idx) | |
| let r1 = r0 *. r0 | |
| let temp = (1.0 -. cosine) | |
| let t1 = temp *. temp | |
| let t2 = t1 *. t1 | |
| let tf = t2 *. temp | |
| r1 +. (1.0 -. r1) *. tf | |
| } | |
| let dielectric = (irate) => { | |
| let ret: material = (. ray, hitre) => { | |
| let color = v3create(1.0, 1.0, 1.0) | |
| let ratio = hitre.front ? 1.0 /. irate : irate | |
| let unit_dir = ray->raydirection->v3unit | |
| let cos_theta = min(-1.0 *. v3dot(unit_dir, hitre.normal), 1.0) | |
| let sin_theta = Js.Math.sqrt(1.0 -. cos_theta *. cos_theta) | |
| let cannot_refract = ratio *. sin_theta > 1.0 | |
| let direction = cannot_refract || reflectance(cos_theta, ratio) > rand() ? | |
| reflect(unit_dir, hitre.normal) : | |
| refract(unit_dir, hitre.normal, ratio) | |
| let scattered = {origin: hitre.p, | |
| direction: direction} | |
| Some(color, scattered) | |
| } | |
| ret | |
| } | |
| let random_scene = () => { | |
| let ground_material = lambertian(v3create(0.5, 0.5, 0.5)) | |
| let world = [] | |
| let add = (element)=>ignore(world->Js.Array2.push(element)) | |
| add(sphere2hittable(v3create(0.0, -1000.0, 0.0), 1000.0, ground_material)) | |
| for a in -11 to 10 { | |
| for b in -11 to 10 { | |
| let fa = a->Belt.Int.toFloat | |
| let fb = b->Belt.Int.toFloat | |
| let choose_mat = rand() | |
| let center = v3create(fa +. 0.9 *. rand(), | |
| 0.2, | |
| fb +. 0.9 *. rand()) | |
| if v3vsub(center, v3create(4.0, 0.2, 0.0))->v3len > 0.9 { | |
| if choose_mat < 0.8 { | |
| let a1 = v3create(rand(), rand(), rand()) | |
| let a2 = v3create(rand(), rand(), rand()) | |
| let albedo = v3vmul(a1, a2) | |
| let m = lambertian(albedo) | |
| add(sphere2hittable(center, 0.2, m)) | |
| } else if choose_mat < 0.95 { | |
| let albedo = v3create(rand() /. 2.0 +. 0.5, | |
| rand() /. 2.0 +. 0.5, | |
| rand() /. 2.0 +. 0.5) | |
| let fuzz = rand() /. 2.0 | |
| let m = metal(albedo, fuzz) | |
| add(sphere2hittable(center, 0.2, m)) | |
| } else { | |
| let m =dielectric(1.5) | |
| add(sphere2hittable(center, 0.2, m)) | |
| } | |
| } | |
| } | |
| } | |
| let m1 = dielectric(1.5) | |
| add(sphere2hittable(v3create(0.0, 1.0, 0.0), 1.0, m1)) | |
| let m2 = lambertian(v3create(0.4, 0.2, 0.1)) | |
| add(sphere2hittable(v3create(-4.0, 1.0, 0.0), 1.0, m2)) | |
| let m3 = metal(v3create(0.7, 0.6, 0.5), 0.0) | |
| add(sphere2hittable(v3create(4.0, 1.0, 0.0), 1.0, m3)) | |
| world | |
| } | |
| let rec ray_color = (r: ray, world: array<hittable>, depth: int, cv) => { | |
| if (depth <= 0) { | |
| v3create(0.0, 0.0, 0.0) | |
| } else { | |
| let somehit = hitByList(. r, 0.001, inf, world) | |
| switch somehit { | |
| | Some(a) => { | |
| let b = a.mater(. r, a) | |
| switch b { | |
| | Some((color, scattered)) => { | |
| ray_color(scattered, world, depth - 1, v3vmul(color, cv)) | |
| } | |
| | None => { | |
| v3create(0.0, 0.0, 0.0) | |
| } | |
| } | |
| } | |
| | None => { | |
| let un = r->raydirection->v3unit | |
| let ti = (v3y(un) +. 1.0) *. 0.5 | |
| let r1 = v3create(1.0, 1.0, 1.0)->v3mul(1.0 -. ti) | |
| let r2 = v3create(0.5, 0.7, 1.0)->v3mul(ti) | |
| let r = v3vadd(r1, r2) | |
| v3vmul(r, cv) | |
| } | |
| } | |
| } | |
| } | |
| let main = () => { | |
| let aspect_ratio = 3.0 /. 2.0 | |
| let image_width = 400 | |
| let image_height = (Belt.Int.toFloat(image_width) /. aspect_ratio) | |
| ->Belt.Float.toInt | |
| let samples_per_pixel = 50 | |
| let fper = samples_per_pixel->Belt.Int.toFloat | |
| let max_depth = 50 | |
| let aperture = 0.1 | |
| let dist_to_focus = 10.0 | |
| let world = random_scene() | |
| let came = camcreate(~lookfrom=v3create(13.0, 2.0, 3.0), | |
| ~lookat=v3create(0.0, 0.0, 0.0), | |
| ~vup=v3create(0.0, 1.0, 0.0), | |
| ~aspect_ratio=aspect_ratio, | |
| ~viewport_height=2.0, | |
| ~vfov=20.0, | |
| ~focus_dist=dist_to_focus, | |
| ~aperture=aperture) | |
| Js.Console.timeStart("test") | |
| Belt.Array.makeBy(image_height, (j) => { | |
| Belt.Array.makeBy(image_width, (i) => { | |
| let j = image_height - 1 - j | |
| let color = v3create(0.0, 0.0, 0.0) | |
| for _ in 0 to samples_per_pixel - 1 { | |
| let u = (Belt.Int.toFloat(i) +. rand()) /. Belt.Int.toFloat(image_width - 1) | |
| let v = (Belt.Int.toFloat(j) +. rand()) /. Belt.Int.toFloat(image_height - 1) | |
| let r = came->camgetray(. _, u, v) | |
| v3vaddInPlace(color, ray_color(r, world, max_depth, v3create(1.0, 1.0, 1.0))) | |
| } | |
| v3create((v3x(color) /. fper)->clamp(0.0, 0.999)->Js.Math.sqrt, | |
| (v3y(color) /. fper)->clamp(0.0, 0.999)->Js.Math.sqrt, | |
| (v3z(color) /. fper)->clamp(0.0, 0.999)->Js.Math.sqrt) | |
| }) | |
| })->writemat("fb.ppm") | |
| Js.Console.timeEnd("test") | |
| } | |
| main() |