From c1bb56bb263872c969bf63a6ba8c3c8858870434 Mon Sep 17 00:00:00 2001 From: Mikhail Turkeev Date: Sun, 14 Jul 2024 21:23:31 +0400 Subject: [PATCH] rewrite camera to use matrices --- src/codegen/mat.mjs | 169 ++++++++++++++++++++- src/js/camera.ts | 186 ++++++++++++------------ src/js/multi_threaded_render.ts | 4 +- src/js/scenes/book-1-final-scene.ts | 2 +- src/js/scenes/book-2-final-scene.ts | 2 +- src/js/scenes/cornell_box.ts | 2 +- src/js/scenes/cornell_box_with_smoke.ts | 2 +- src/js/scenes/damaged_helmet_gltf.ts | 2 +- src/js/scenes/earth.ts | 2 +- src/js/scenes/scene.ts | 2 +- src/js/scenes/simple_gltf.ts | 2 +- src/js/scenes/simple_light.ts | 2 +- src/js/scenes/two_perlin_spheres.ts | 2 +- src/js/scenes/two_spheres.ts | 2 +- src/js/single_threaded_render.ts | 2 +- todo.md | 1 - 16 files changed, 270 insertions(+), 114 deletions(-) diff --git a/src/codegen/mat.mjs b/src/codegen/mat.mjs index 94030ce..8c01a24 100644 --- a/src/codegen/mat.mjs +++ b/src/codegen/mat.mjs @@ -9,8 +9,8 @@ const gen_type_name = ({rows, cols}) => { : `Mat${rows}x${cols}`; }; -const get_idx_fn = (t) => { - return (row_index, col_index) => col_index * t.rows + row_index; +const get_idx_fn = (matrix_layout) => { + return (row_index, col_index) => col_index * matrix_layout.rows + row_index; }; // we follow the standard math notation, where m12 is an element on row 1 and column 2 @@ -586,9 +586,160 @@ export const gen_transpose = (template) => (use_result_arg) => { return gen_fn(name, signature, body, use_result_arg); } +export const gen_gl_asymmetric_perspective_projection = (use_result_arg) => { + const name = 'gl_asymmetric_perspective_projection_to_mat4'; + const signature = gen_signature(use_result_arg, sig('Mat4', 'left: number, right: number, bottom: number, top: number, near: number, far: number')); + const preamble = [ + 'const near2 = near * 2;', + 'const inv_width = 1 / (right - left);', + 'const inv_height = 1 / (top - bottom);', + 'let m10, m14;', + 'if (Number.isFinite(far)) {', + ' const inv_neg_depth = 1 / (near - far);', + ' m10 = (near + far) * inv_neg_depth;', + ' m14 = near2 * far * inv_neg_depth;', + '} else {', + ' m10 = -1;', + ' m14 = -near2;', + '}', + ].map(x => ind + x).join('\n'); + + const components = [ + 'near2 * inv_width', + '0', + '0', + '0', + '0', + 'near2 * inv_height', + '0', + '0', + '(left + right) * inv_width', + '(bottom + top) * inv_height', + 'm10', + '-1', + '0', + '0', + 'm14', + '0', + ]; + const body = [ + preamble, + gen_output(use_result_arg, 'mat4', components) + ].join('\n\n'); + + return gen_fn(name, signature, body, use_result_arg); +}; + +export const gen_gl_perspective_projection = (use_result_arg) => { + const name = `gl_perspective_projection`; + const signature = gen_signature(use_result_arg, sig('Mat4', 'aspect: number, y_fov: number, near: number, far: number')); + const preamble = [ + ind + 'const top = near * Math.tan(y_fov * 0.5);', + ind + 'const right = top * aspect;', + ].join('\n'); + + const body = [ + preamble, + use_result_arg + ? ind + `gl_asymmetric_perspective_projection_to_mat4_r(result, -right, right, -top, top, near, far);` + : ind + `return gl_asymmetric_perspective_projection_to_mat4(-right, right, -top, top, near, far);` + ].join('\n\n'); + + return gen_fn(name, signature, body, use_result_arg); +} + +export const gen_look_direction_to_mat = (matrix_layout) => (use_result_arg) => { + const mat_type_name = gen_type_name(matrix_layout); + + const mat_name = mat_type_name.toLowerCase(); + + const fn_name = `look_direction_to_${mat_name}`; + const signature = gen_signature(use_result_arg, sig(mat_type_name, `direction: Vec3, up: Vec3`)); + + const preamble = [ + 'const x = vec3_dirty();', + 'const y = vec3_dirty();', + 'const z = vec3_dirty();', + '', + 'z.set(direction);', + 'unit_vec3_r(z, z);', + '', + 'if (!z.every(Number.isFinite)) {', + 'orthogonal_vec3_r(z, up);', + 'unit_vec3_r(z, z);', + '} else {', + 'negate_vec3_r(z, z);', + '}', + '', + 'cross_vec3_r(x, up, z);', + 'unit_vec3_r(x, x);', + 'if (!x.every(Number.isFinite)) {', + 'orthogonal_vec3_r(x, z);', + '}', + 'cross_vec3_r(y, z, x);' + ].map(x => ind + x).join('\n'); + + const components = [ + 'x[0]', + 'x[1]', + 'x[2]', + '0', + 'y[0]', + 'y[1]', + 'y[2]', + '0', + 'z[0]', + 'z[1]', + 'z[2]', + '0', + '0', + '0', + '0', + '1', + ].filter((x, i) => { + const col_index = Math.floor(i / 4); + const row_index = i % 4; + return matrix_layout.template[row_index][col_index] === s; + }); + + const body = [ + preamble, + gen_output(use_result_arg, mat_name, components) + ].join('\n\n'); + + return gen_fn(fn_name, signature, body, use_result_arg); +} + +export const gen_look_target_to_mat = (matrix_layout) => (use_result_arg) => { + const mat_type_name = gen_type_name(matrix_layout); + + const mat_name = mat_type_name.toLowerCase(); + + const fn_name = `look_target_to_${mat_name}`; + const signature = gen_signature(use_result_arg, sig(mat_type_name, `origin: Vec3, target: Vec3, up: Vec3`)); + const get_idx = get_idx_fn(matrix_layout); + + const body = [ + // const direction = new Vector3().fromVector(target).subtractVector(origin); + 'const direction = sub_vec3(target, origin);', + use_result_arg + ? `look_direction_to_${mat_name}_r(result, direction, up);` + : `const result = look_direction_to_${mat_name}(direction, up);`, + `result[${get_idx(0, 3)}] = origin[0];`, + `result[${get_idx(1, 3)}] = origin[1];`, + `result[${get_idx(2, 3)}] = origin[2];`, + ...(use_result_arg + ? [] + : ['return result;'] + ) + ].map(x => ind + x).join('\n'); + + return gen_fn(fn_name, signature, body, use_result_arg); +} + export const gen_mat_module = () => { const module_code = [ - `import {Vec3, vec3} from './vec3.gen'`, + `import {Vec3, vec3, vec3_dirty, unit_vec3_r, orthogonal_vec3_r, negate_vec3_r, cross_vec3_r, sub_vec3} from './vec3.gen'`, `import {Quat} from './quat.gen'`, `import { run_hook } from '../utils';`, gen_mat_preamble(lin), @@ -633,7 +784,17 @@ export const gen_mat_module = () => { gen_trs_to_mat(lin), gen_trs_to_mat(aff), - gen_trs_to_mat(hom) + gen_trs_to_mat(hom), + + gen_gl_asymmetric_perspective_projection, + gen_gl_perspective_projection, + + gen_look_direction_to_mat(lin), + gen_look_direction_to_mat(aff), + gen_look_direction_to_mat(hom), + + gen_look_target_to_mat(aff), + gen_look_target_to_mat(hom), ].flatMap(f => [f(false), f(true)]) ].join('\n\n') + '\n'; diff --git a/src/js/camera.ts b/src/js/camera.ts index ce59de6..48ef397 100644 --- a/src/js/camera.ts +++ b/src/js/camera.ts @@ -1,117 +1,113 @@ import { - add_vec3, add_vec3_r, - cross_vec3, cross_vec3_r, div_vec3_s, fma_vec3_s_vec3_r, mul_vec3_s, mul_vec3_s_r, - Point3, point3_dirty, rand_vec3_in_unit_disk, sub_vec3, sub_vec3_r, unit_vec3, unit_vec3_r, - Vec3, vec3_dirty + gl_perspective_projection, + invert_mat4_r, + look_target_to_mat3x4_r, + Mat3x4, mat3x4_dirty, + Mat4, mat4_dirty, mul_mat3_vec3_r, mul_mat3x4_vec3_r, + mul_mat4_vec3_r +} from './math/mat3.gen'; +import { + mul_vec3_s_r, + point3, + Point3, + rand_vec3_in_unit_disk, + sub_vec3, + sub_vec3_r, + Vec3 } from './math/vec3.gen'; -import { Ray, ray_allocator, ray_dirty, ray_set } from './math/ray'; +import { Ray, ray_dirty, ray_set } from './math/ray'; import { degrees_to_radians } from './utils'; -import { random_min_max } from './math/random'; - -export interface CameraConfig { - look_from: Point3, - look_at: Point3, - v_up: Vec3, - y_fov: number, - aperture: number, - focus_dist: number, - time0: number, - time1: number -} - -const ray = ray_dirty(); export interface Camera { - origin: Point3; - lower_left_corner: Point3; - vertical: Vec3; - horizontal: Vec3; - u: Vec3; - v: Vec3; - w: Vec3; - lens_radius: number; + world_matrix: Mat3x4; + projection_matrix_inverse: Mat4; + + //depth of field + origin_radius: number, + + //motion blur + t0: number; + dt: number; + + config: Camera2Config; +} + +export interface Camera2Config { + //view + y_up: Vec3; + look_from: Point3; + look_at: Point3; + //projection + y_fov: number; + //depth of field + aperture: number; + focus_dist: number; + //motion blur time0: number; time1: number; - - config: CameraConfig; } -export const create_camera = (config: CameraConfig): Camera => { +export const create_camera = (config: Camera2Config): Camera => { return { - origin: point3_dirty(), - lower_left_corner: point3_dirty(), - vertical: vec3_dirty(), - horizontal: vec3_dirty(), - u: vec3_dirty(), - v: vec3_dirty(), - w: vec3_dirty(), - lens_radius: NaN, - time0: NaN, - time1: NaN, - + world_matrix: mat3x4_dirty(), + projection_matrix_inverse: mat4_dirty(), + origin_radius: 0, + t0: 0, + dt: 0, config - } -}; + }; +} -export const configure_camera = (camera: Camera, aspect_ratio: number): void => { - const { - look_from, - look_at, - v_up, - y_fov, - aperture, - focus_dist, - time0, - time1 - } = camera.config; - - const theta = degrees_to_radians(y_fov); - const h = Math.tan(theta / 2); - const viewport_height = 2 * h; - const viewport_width = aspect_ratio * viewport_height; - - unit_vec3_r(camera.w, sub_vec3(look_from, look_at)); - unit_vec3_r(camera.u, cross_vec3(v_up, camera.w)); - cross_vec3_r(camera.v, camera.w, camera.u); - - camera.time0 = time0; - camera.time1 = time1; - - camera.origin.set(look_from); - mul_vec3_s_r(camera.horizontal, camera.u, viewport_width * focus_dist); - mul_vec3_s_r(camera.vertical, camera.v, viewport_height * focus_dist); - sub_vec3_r( - camera.lower_left_corner, - camera.origin, - add_vec3( - add_vec3( - div_vec3_s(camera.horizontal, 2), - div_vec3_s(camera.vertical, 2) - ), - mul_vec3_s(camera.w, focus_dist) - ) +export const configure_camera = (camera: Camera, aspect: number) => { + const config = camera.config; + look_target_to_mat3x4_r(camera.world_matrix, config.look_from, config.look_at, config.y_up); + const projection_matrix = gl_perspective_projection( + aspect, + degrees_to_radians(config.y_fov), + //near=1, far=1+focus_dist + //so when we map a direction from point(x, y, -1) to point(x, y, 1) we get a direction from 0 to (x, y) on the focus plane + 1, 1 + config.focus_dist ); - camera.lens_radius = aperture / 2; + invert_mat4_r(camera.projection_matrix_inverse, projection_matrix); + + camera.origin_radius = config.aperture / 2; + camera.dt = config.time1 - config.time0; + camera.t0 = config.time0; }; +const ray = ray_dirty(); export const get_ray = (camera: Camera, u: number, v: number): Ray => { - const rd = rand_vec3_in_unit_disk(); - mul_vec3_s_r(rd, rd, camera.lens_radius); - const offset = mul_vec3_s(camera.u, rd[0]); - fma_vec3_s_vec3_r(offset, camera.v, rd[1], offset) + const ndc_u = u * 2 - 1; + const ndc_v = 1 - 2 * v; + + //"from to" in NDC + const from = point3(ndc_u, ndc_v, -1); + const to = point3(ndc_u, ndc_v, 1); + + //transform "from to" to camera's space + mul_mat4_vec3_r(from, camera.projection_matrix_inverse, from); + mul_mat4_vec3_r(to, camera.projection_matrix_inverse, to); + + // compute ray direction in camera's space + const dir = sub_vec3(to, from); - const direction = sub_vec3(camera.lower_left_corner, offset); - add_vec3_r(direction, direction, mul_vec3_s(camera.horizontal, u)); - add_vec3_r(direction, direction, mul_vec3_s(camera.vertical, v)); - sub_vec3_r(direction, direction, camera.origin); + //ray origin in camera space is 0,0,0, but we make it slightly random for DoF + const origin = rand_vec3_in_unit_disk();//todo: non-circular aperture + mul_vec3_s_r(origin, origin, camera.origin_radius); + + // since origin is not 0,0,0 because DoF, ray no longer points to correct location. + // Need to compensate for origin offset. + sub_vec3_r(dir, dir, origin); + + //transform origin and dir to world_space + mul_mat3x4_vec3_r(origin, camera.world_matrix, origin); + mul_mat3_vec3_r(dir, camera.world_matrix, dir);//note: we abuse the fact that matrices are column-major, therefore we can interpret Mat3x4 as Mat3 in this case. - add_vec3_r(offset, offset, camera.origin); ray_set( ray, - offset, - direction, - random_min_max(camera.time0, camera.time1) + origin, + dir, + camera.t0 + camera.dt * Math.random() ); - return ray; -}; +} diff --git a/src/js/multi_threaded_render.ts b/src/js/multi_threaded_render.ts index 82be53a..691609e 100644 --- a/src/js/multi_threaded_render.ts +++ b/src/js/multi_threaded_render.ts @@ -72,11 +72,11 @@ export async function multi_threaded_render({render_parameters, thread_count, wr tmp_color[0] = output_buffer[w_offset + j * 3] += pixels[r_offset + j * 3]; tmp_color[1] = output_buffer[w_offset + j * 3 + 1] += pixels[r_offset + j * 3 + 1]; tmp_color[2] = output_buffer[w_offset + j * 3 + 2] += pixels[r_offset + j * 3 + 2]; - write_color(x + j, (image_height - y - i - 1), tmp_color, rays_casted_per_tile[tile_index], color_flow); + write_color(x + j, y + i, tmp_color, rays_casted_per_tile[tile_index], color_flow); } } - dump_tile(x, (image_height - y - height), width, height); + dump_tile(x, y, width, height); const dt = performance.now() - t0; progress_reporter.report(thread_id, progress, samples_per_pixel * width * height, total_rays, dt); diff --git a/src/js/scenes/book-1-final-scene.ts b/src/js/scenes/book-1-final-scene.ts index b06e0e7..d46db91 100644 --- a/src/js/scenes/book-1-final-scene.ts +++ b/src/js/scenes/book-1-final-scene.ts @@ -82,7 +82,7 @@ export const create = (): Scene => create_scene({ camera: create_camera({ look_from: point3(13, 2, 3), look_at: point3(0, 0, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0.1, y_fov: 20, diff --git a/src/js/scenes/book-2-final-scene.ts b/src/js/scenes/book-2-final-scene.ts index ce215be..14ab684 100644 --- a/src/js/scenes/book-2-final-scene.ts +++ b/src/js/scenes/book-2-final-scene.ts @@ -106,7 +106,7 @@ export const create = async (): Promise => { camera: create_camera({ look_from: point3(478, 278, -600), look_at: point3(278, 278, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 40, diff --git a/src/js/scenes/cornell_box.ts b/src/js/scenes/cornell_box.ts index 8417168..06b508e 100644 --- a/src/js/scenes/cornell_box.ts +++ b/src/js/scenes/cornell_box.ts @@ -75,7 +75,7 @@ export const create = (): Scene => { camera: create_camera({ look_from: point3(278, 278, -800), look_at: point3(278, 278, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 40, diff --git a/src/js/scenes/cornell_box_with_smoke.ts b/src/js/scenes/cornell_box_with_smoke.ts index e3ab1dd..6e1a015 100644 --- a/src/js/scenes/cornell_box_with_smoke.ts +++ b/src/js/scenes/cornell_box_with_smoke.ts @@ -66,7 +66,7 @@ export const create = () => { camera: create_camera({ look_from: point3(278, 278, -800), look_at: point3(278, 278, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 40, diff --git a/src/js/scenes/damaged_helmet_gltf.ts b/src/js/scenes/damaged_helmet_gltf.ts index a92022b..4f02218 100644 --- a/src/js/scenes/damaged_helmet_gltf.ts +++ b/src/js/scenes/damaged_helmet_gltf.ts @@ -15,7 +15,7 @@ export const create = async (): Promise => { camera: create_camera({ look_from: point3(3, -1, 3), look_at: point3(0, 0, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 3.8, aperture: 0.05, y_fov: 30, diff --git a/src/js/scenes/earth.ts b/src/js/scenes/earth.ts index 03575bf..2db6642 100644 --- a/src/js/scenes/earth.ts +++ b/src/js/scenes/earth.ts @@ -18,7 +18,7 @@ export const create = async (): Promise => { camera: create_camera({ look_from: point3(13, 2, 3), look_at: point3(0, 0, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0.1, y_fov: 20, diff --git a/src/js/scenes/scene.ts b/src/js/scenes/scene.ts index 4ee0c01..14cccd2 100644 --- a/src/js/scenes/scene.ts +++ b/src/js/scenes/scene.ts @@ -20,7 +20,7 @@ export const create_scene = (config: Partial): Scene => { camera: config.camera ?? create_camera({ look_from: point3(13, 2, 3), look_at: point3(0, 0, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 20, diff --git a/src/js/scenes/simple_gltf.ts b/src/js/scenes/simple_gltf.ts index 89df1c9..6c509c6 100644 --- a/src/js/scenes/simple_gltf.ts +++ b/src/js/scenes/simple_gltf.ts @@ -15,7 +15,7 @@ export const create = async (): Promise => { camera: create_camera({ look_from: point3(-5, 10, 5), look_at: point3(1, 1, -1), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 50, diff --git a/src/js/scenes/simple_light.ts b/src/js/scenes/simple_light.ts index ec0dafd..2b28276 100644 --- a/src/js/scenes/simple_light.ts +++ b/src/js/scenes/simple_light.ts @@ -30,7 +30,7 @@ export const create = (): Scene => { camera: create_camera({ look_from: point3(26, 3, 6), look_at: point3(0, 2, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 20, diff --git a/src/js/scenes/two_perlin_spheres.ts b/src/js/scenes/two_perlin_spheres.ts index 8f57908..82556c2 100644 --- a/src/js/scenes/two_perlin_spheres.ts +++ b/src/js/scenes/two_perlin_spheres.ts @@ -19,7 +19,7 @@ export const create = () => { camera: create_camera({ look_from: point3(13, 2, 3), look_at: point3(0, 0, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 20, diff --git a/src/js/scenes/two_spheres.ts b/src/js/scenes/two_spheres.ts index e60b78a..b090fc8 100644 --- a/src/js/scenes/two_spheres.ts +++ b/src/js/scenes/two_spheres.ts @@ -23,7 +23,7 @@ export const create = (): Scene => create_scene({ camera: create_camera({ look_from: point3(13, 2, 3), look_at: point3(0, 0, 0), - v_up: vec3(0, 1, 0), + y_up: vec3(0, 1, 0), focus_dist: 10, aperture: 0, y_fov: 20, diff --git a/src/js/single_threaded_render.ts b/src/js/single_threaded_render.ts index 804aaec..889e9ab 100644 --- a/src/js/single_threaded_render.ts +++ b/src/js/single_threaded_render.ts @@ -44,7 +44,7 @@ export async function single_threaded_render(parameters: RenderParameters, write tmp_color[0] = output_buffer[w_offset + j * 3] += pixels[r_offset + j * 3]; tmp_color[1] = output_buffer[w_offset + j * 3 + 1] += pixels[r_offset + j * 3 + 1]; tmp_color[2] = output_buffer[w_offset + j * 3 + 2] += pixels[r_offset + j * 3 + 2]; - write_color(x + j, (image_height - y - i - 1), tmp_color, rays_casted_per_tile[tile_index], color_flow); + write_color(x + j, y + i, tmp_color, rays_casted_per_tile[tile_index], color_flow); } } diff --git a/todo.md b/todo.md index d94d816..a63b676 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,4 @@ - **bug** Sometimes according to normal-map or/and vertex normals, combined with micro-facets distribution, reflected ray must go below the surface. This is currently rendered as black. Should probably do something smarter in that case. -- matrix-based camera - hitting bvh with triangles is too slow. Need more efficient in-memory structure after loading gltf. This may also help with messy UV/vertex-normals related code. - pdf mixer with explicit weights. When using image based importance sampling with PBR material, specular/diffuse rays will get only quarter priority instead of one third. More general mixer may fix that. - configuration UI