@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
604 lines (451 loc) • 17.5 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import { BVH, NULL_NODE } from "../../../../core/bvh2/bvh3/BVH.js";
import { ebvh_build_hierarchy } from "../../../../core/bvh2/bvh3/ebvh_build_hierarchy.js";
import { ebvh_optimize_treelet } from "../../../../core/bvh2/bvh3/ebvh_optimize_treelet.js";
import { bvh_query_user_data_ray_segment } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_ray_segment.js";
import { Cache } from "../../../../core/cache/Cache.js";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import { array_quick_sort_by_lookup_map } from "../../../../core/collection/array/array_quick_sort_by_lookup_map.js";
import { strictEquals } from "../../../../core/function/strictEquals.js";
import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js";
import { v3_morton_encode_bounded } from "../../../../core/geom/3d/morton/v3_morton_encode_bounded.js";
import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
import { ray3_array_compose } from "../../../../core/geom/3d/ray/ray3_array_compose.js";
import { v3_dot } from "../../../../core/geom/vec3/v3_dot.js";
import { v3_length } from "../../../../core/geom/vec3/v3_length.js";
import { saturate } from "../../../../core/math/clamp01.js";
import { max2 } from "../../../../core/math/max2.js";
import { compute_geometry_polycount } from "../../geometry/compute_geometry_polycount.js";
import { AbstractLight } from "../../render/forward_plus/model/AbstractLight.js";
import { BufferedGeometryBVH } from "./BufferedGeometryBVH.js";
import { make_vector3 } from "./make_one_vector3.js";
import { MaterialConverter } from "./material/MaterialConverter.js";
import { PathTracedMesh } from "./PathTracedMesh.js";
import { sample_material } from "./texture/sample_material.js";
function light_getDistanceAttenuation(distance, cutoff_distance, decay_exponent) {
let distanceFalloff = 1.0 / Math.max(Math.pow(distance, decay_exponent), 0.01);
if (cutoff_distance > 0.0) {
const b = distance / cutoff_distance;
const b2 = b * b;
const b4 = b2 * b2;
const c = saturate(1.0 - b4);
distanceFalloff *= c * c;
}
return distanceFalloff;
}
/**
*
* @type {number[]|Uint32Array}
*/
const scratch_uint32_array = new Uint32Array(4096);
const _ray_1 = new Ray3();
const DEFAULT_CONVERTER = new MaterialConverter();
export class PathTracedScene {
/**
*
* @type {BVH}
*/
bvh_top_level = new BVH();
/**
*
* @type {Map<number, PathTracedMesh>}
*/
meshes = new Map();
/**
*
* @type {AbstractLight[]}
*/
__lights = [];
/**
*
* @type {Map<THREE.BufferGeometry, BufferedGeometryBVH>}
*/
geo_cache = new Map();
/**
*
* @type {Map<PathTracedMesh, number>}
*/
#mesh_bvh_nodes = new Map();
/**
*
* @type {Cache<THREE.Material, StandardMaterial>}
*/
#material_cache = new Cache({
keyEqualityFunction: strictEquals,
keyHashFunction(mat) {
return mat.id
}
});
/**
*
* @type {function}
* @private
*/
__background_sampler = make_vector3(0, 0, 0);
/**
*
* @returns {PathTracedMesh[]}
*/
get #meshes() {
return Array.from(this.meshes.values());
}
#rebuild_bvh() {
const meshes = this.#meshes;
const bounds = new AABB3();
const bvh = this.bvh_top_level;
if (bvh.root === NULL_NODE) {
return;
}
bvh.node_get_aabb(bvh.root, bounds);
bvh.release_all();
const mesh_count = meshes.length;
const morton_codes = new Map();
for (let i = 0; i < mesh_count; i++) {
const mesh = meshes[i];
const center = mesh.aabb.getCenter();
const code = v3_morton_encode_bounded(center.x, center.y, center.z, bounds);
morton_codes.set(mesh, code);
}
//
array_quick_sort_by_lookup_map(meshes, morton_codes, 0, mesh_count - 1);
const node_leaf_count = mesh_count;
const node_bin_count = max2(0, node_leaf_count - 1);
const node_total_count = node_leaf_count + node_bin_count;
const nodes = new Uint32Array(node_total_count);
// skip allocation calls, allocate exactly as many nodes as we need
bvh.node_capacity = node_total_count;
bvh.__size = node_total_count;
for (let i = 0; i < node_total_count; i++) {
// store nodes in reverse order so that top-level nodes end up on top
nodes[i] = (node_total_count - 1) - i;
}
// assign leaves
for (let i = 0; i < node_leaf_count; i++) {
const node = nodes[i];
const mesh = meshes[i];
bvh.node_set_aabb(node, mesh.aabb);
bvh.node_set_child1(node, NULL_NODE); //mark node as a leaf
bvh.node_set_user_data(node, mesh.id);
bvh.node_set_height(node, 0);
}
// record newly generated nodes as "unprocessed"
const unprocessed_nodes = new Uint32Array(node_leaf_count);
array_copy(nodes, 0, unprocessed_nodes, 0, node_leaf_count);
// assign root
bvh.__root = ebvh_build_hierarchy(bvh, unprocessed_nodes, node_leaf_count, nodes, node_leaf_count,16);
ebvh_optimize_treelet(bvh);
}
optimize() {
this.meshes.forEach((mesh, mesh_id) => {
mesh.build_tight_bounds();
});
// re-build bvh from bottom up
this.#rebuild_bvh();
}
/**
*
* @param {THREE.BufferGeometry} geo
* @return {BufferedGeometryBVH}
*/
obtainGeometryBVH(geo) {
const cached = this.geo_cache.get(geo);
if (cached !== undefined) {
return cached;
}
const bvh = new BufferedGeometryBVH();
const label = `bvh build ${compute_geometry_polycount(geo)}`;
console.time(label);
bvh.build(geo);
console.timeEnd(label);
this.geo_cache.set(geo, bvh);
return bvh;
}
/**
*
* @param {AbstractLight} light
*/
addLight(light) {
assert.isInstanceOf(light, AbstractLight, 'light');
this.__lights.push(light);
}
/**
*
* @param {PathTracedMesh} mesh
* @returns {boolean}
*/
hasMesh(mesh) {
return this.#mesh_bvh_nodes.has(mesh);
}
/**
*
* @param {PathTracedMesh} mesh
* @returns {boolean}
*/
addMesh(mesh) {
if (this.hasMesh(mesh)) {
// already exists
return false;
}
mesh.bvh = this.obtainGeometryBVH(mesh.geometry);
mesh.update_bounds();
const bvh = this.bvh_top_level;
const bvh_node_id = bvh.allocate_node();
this.#mesh_bvh_nodes.set(mesh, bvh_node_id);
bvh.node_set_aabb(
bvh_node_id,
mesh.aabb
);
bvh.node_set_user_data(
bvh_node_id,
mesh.id
);
bvh.insert_leaf(bvh_node_id);
this.meshes.set(mesh.id, mesh);
return true;
}
/**
*
* @param {PathTracedMesh} mesh
* @returns {boolean}
*/
removeMesh(mesh) {
if (!this.hasMesh(mesh)) {
return false;
}
const node_id = this.#mesh_bvh_nodes.get(mesh);
this.#mesh_bvh_nodes.delete(mesh);
this.bvh_top_level.remove_leaf(node_id);
this.bvh_top_level.release_node(node_id);
return true;
}
/**
* Retrieves pre-cached material or build one from scratch, caches it and returns the result
* @param {THREE.Material} material
* @returns {StandardMaterial}
*/
obtainMaterial(material) {
return this.#material_cache.getOrCompute(material, DEFAULT_CONVERTER.convert, DEFAULT_CONVERTER);
}
/**
*
* @param {THREE.BufferGeometry} geometry
* @param {THREE.Material} material
* @param {mat4|number[]} transform
*/
createMesh(geometry, material, transform) {
const standard_material = this.obtainMaterial(material);
const mesh = new PathTracedMesh();
mesh.geometry = geometry;
mesh.material = standard_material;
mesh.transform = transform;
this.addMesh(mesh);
}
/**
*
* @param {number[]} out [color_r, color_g, color_b, normal_x, normal_y, normal_z]
* @param {number[]} hit
* @param {Ray3} incoming_ray
*/
sample_material(out, hit, incoming_ray) {
const primitive_id = hit[9];
const instance_id = hit[10];
const u = hit[7];
const v = hit[8];
out[0] = 1;
out[1] = 1;
out[2] = 1;
/**
*
* @type {PathTracedMesh}
*/
const mesh = this.meshes.get(instance_id);
if (mesh === undefined) {
// instance not found
return;
}
sample_material(out, mesh, primitive_id, u, v);
if (v3_dot(
hit[3], hit[4], hit[5], //geometry nornal
incoming_ray[3], incoming_ray[4], incoming_ray[5],
) > 0) {
// back-facing triangle
out[3] = -out[3];
out[4] = -out[4];
out[5] = -out[5];
}
// const pdf = mesh.material.scattering_pdf(
// incoming_ray[3],incoming_ray[4],incoming_ray[5],
// out[3],out[4],out[5],
// incoming_ray[3],incoming_ray[4],incoming_ray[5],
// );
}
/**
* Tests ray for occlusion
* @param {Ray3} ray
* @returns {boolean}
*/
occluded(ray) {
const bvh = this.bvh_top_level;
const ray_origin_x = ray[0];
const ray_origin_y = ray[1];
const ray_origin_z = ray[2];
const ray_direction_x = ray[3];
const ray_direction_y = ray[4];
const ray_direction_z = ray[5];
const max_distance = ray[6];
const hit_count = bvh_query_user_data_ray_segment(
bvh, bvh.root,
scratch_uint32_array, 0,
ray_origin_x, ray_origin_y, ray_origin_z,
ray_direction_x, ray_direction_y, ray_direction_z,
0, max_distance
);
for (let i = 0; i < hit_count; i++) {
const node_user_data = scratch_uint32_array[i];
const mesh = this.meshes.get(node_user_data);
if (mesh.occluded(ray)) {
return true;
}
}
return false;
}
/**
*
* @param {number[]} out
* @param {number[]|Ray3} ray
* @return {number} distance to contact, or -1 if no contact found
*/
trace(out, ray) {
const bvh = this.bvh_top_level;
const ray_origin_x = ray[0];
const ray_origin_y = ray[1];
const ray_origin_z = ray[2];
const ray_direction_x = ray[3];
const ray_direction_y = ray[4];
const ray_direction_z = ray[5];
const max_distance = ray[6];
const hit_count = bvh_query_user_data_ray_segment(
bvh, bvh.root,
scratch_uint32_array, 0,
ray_origin_x, ray_origin_y, ray_origin_z,
ray_direction_x, ray_direction_y, ray_direction_z,
0, max_distance
);
let nearest_hit_distance = max_distance;
for (let i = 0; i < hit_count; i++) {
const node_user_data = scratch_uint32_array[i];
const mesh = this.meshes.get(node_user_data);
const distance_to_hit = mesh.hit(out, ray, nearest_hit_distance);
if (distance_to_hit >= 0) {
// since raycast in leaf nodes is already bound by maximum distance, any hit we get is necessarily a closer hit than before
nearest_hit_distance = distance_to_hit;
}
}
if (nearest_hit_distance !== max_distance) {
return nearest_hit_distance;
}
return -1;
}
/**
*
* @param {number[]} out
* @param {number} out_offset
* @param {number[]} direction
* @param {number} direction_offset
*/
sample_background(out, out_offset, direction, direction_offset) {
this.__background_sampler(out, out_offset, direction, direction_offset);
}
/**
*
* @param {number[]} out
* @param {number} out_offset
* @param {number[]} ray
*/
sample_lights(out, out_offset, ray) {
const lights = this.__lights;
const light_count = lights.length;
for (let i = 0; i < 3; i++) {
// initialize contribution to 0
out[out_offset + i] = 0;
}
const ray_origin_x = ray[0];
const ray_origin_y = ray[1];
const ray_origin_z = ray[2];
const ray_direction_x = ray[3];
const ray_direction_y = ray[4];
const ray_direction_z = ray[5];
for (let i = 0; i < light_count; i++) {
const light = lights[i];
if (light.isDirectionalLight === true) {
const dir = light.direction;
// see https://github.com/mrdoob/three.js/blob/f0a9e0cf90a2f1ba5017fcb7fd46f02748b920cf/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js#L172
const light_dir_inv_x = -dir.x;
const light_dir_inv_y = -dir.y;
const light_dir_inv_z = -dir.z;
const dotNL = v3_dot(
ray_direction_x, ray_direction_y, ray_direction_z,
light_dir_inv_x, light_dir_inv_y, light_dir_inv_z
);
if (dotNL <= 0) {
// no contribution, facing away from the light
continue;
}
ray3_array_compose(_ray_1,
ray_origin_x, ray_origin_y, ray_origin_z,
light_dir_inv_x, light_dir_inv_y, light_dir_inv_z
);
_ray_1.tMax = Infinity;
// check if there are any obstacles in the direction of the light
if (this.occluded(_ray_1)) {
// light is occluded
continue;
}
const intensity = dotNL * light.intensity.getValue();
const light_color = light.color;
out[out_offset] += light_color.r * intensity;
out[out_offset + 1] += light_color.g * intensity;
out[out_offset + 2] += light_color.b * intensity;
} else if (light.isPointLight === true) {
const light_offset_x = ray_origin_x - light.position.x;
const light_offset_y = ray_origin_y - light.position.y;
const light_offset_z = ray_origin_z - light.position.z;
const distance_to_light = v3_length(light_offset_x, light_offset_y, light_offset_z);
const radius = light.radius.getValue();
if (distance_to_light > radius) {
// too far
continue;
}
const light_norm_inv = 1 / distance_to_light;
// see https://github.com/mrdoob/three.js/blob/f0a9e0cf90a2f1ba5017fcb7fd46f02748b920cf/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js#L172
const light_dir_inv_x = -light_offset_x * light_norm_inv;
const light_dir_inv_y = -light_offset_y * light_norm_inv;
const light_dir_inv_z = -light_offset_z * light_norm_inv;
const dotNL = v3_dot(
ray_direction_x, ray_direction_y, ray_direction_z,
light_dir_inv_x, light_dir_inv_y, light_dir_inv_z
);
if (dotNL <= 0) {
// no contribution, facing away from the light
continue;
}
ray3_array_compose(_ray_1,
ray_origin_x, ray_origin_y, ray_origin_z,
light_dir_inv_x, light_dir_inv_y, light_dir_inv_z
);
_ray_1.tMax = radius;
// check if there are any obstacles in the direction of the light
if (this.occluded(_ray_1)) {
// light is occluded
continue;
}
const attenuation = light_getDistanceAttenuation(distance_to_light, radius, 2);
const intensity = dotNL * light.intensity.getValue() * attenuation;
const light_color = light.color;
out[out_offset] += light_color.r * intensity;
out[out_offset + 1] += light_color.g * intensity;
out[out_offset + 2] += light_color.b * intensity;
}
}
}
}