@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
874 lines (690 loc) • 27.6 kB
JavaScript
import {
AdditiveBlending,
ClampToEdgeWrapping,
DataTexture,
Matrix3,
MeshStandardMaterial,
MultiplyBlending,
NormalBlending
} from "three";
import { BinaryDataType } from "../../../../core/binary/type/BinaryDataType.js";
import {
compute_typed_array_constructor_from_data_type
} from "../../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
import { array_push_if_unique } from "../../../../core/collection/array/array_push_if_unique.js";
import {
compute_binary_data_type_from_typed_array
} from "../../../../core/collection/array/typed/compute_binary_data_type_from_typed_array.js";
import { collectIteratorValueToArray } from "../../../../core/collection/collectIteratorValueToArray.js";
import { HashMap } from "../../../../core/collection/map/HashMap.js";
import AABB2 from "../../../../core/geom/AABB2.js";
import { MaxRectanglesPacker } from "../../../../core/geom/packing/max-rect/MaxRectanglesPacker.js";
import { computeMaterialEquality } from "../../../asset/loaders/material/computeMaterialEquality.js";
import { computeMaterialHash } from "../../../asset/loaders/material/computeMaterialHash.js";
import { TextureAttachmentsByMaterialType } from "../../../asset/loaders/material/TextureAttachmensByMaterialType.js";
import { traverseThreeObject } from "../../ecs/highlight/renderer/traverseThreeObject.js";
import { channelCountToThreeTextureFormat } from "../../texture/channelCountToThreeTextureFormat.js";
import { computeThreeTextureTypeFromDataType } from "../../texture/computeThreeTextureTypeFromDataType.js";
import { normalized_internal_format } from "../../texture/normalized_internal_format.js";
import { convertTexture2Sampler2D } from "../../texture/sampler/convertTexture2Sampler2D.js";
import { Sampler2D } from "../../texture/sampler/Sampler2D.js";
import { sampler2d_copy_channel_data } from "../../texture/sampler/sampler2d_copy_channel_data.js";
import { sampler2d_sub_copy_same_item_size } from "../../texture/sampler/sampler2d_sub_copy_same_item_size.js";
import { sampler2d_transfer_data } from "../../texture/sampler/sampler2d_transfer_data.js";
import { BUFFER_GEOMETRY_UVS } from "./BUFFER_GEOMETRY_UVS.js";
import { is_compliant_mesh } from "./is_compliant_mesh.js";
import { MaterialDescriptor } from "./MaterialDescriptor.js";
import { prepare_atlas_texture } from "./prepare_atlas_texture.js";
const ATLAS_PATCH_BORDER = 0;
/**
*
* @param {THREE.Material} a
* @param {THREE.Material} b
* @returns {boolean}
*/
function materialsSimilarForCharting(a, b) {
return a.visible === b.visible
&& a.transparent === b.transparent
&& a.blending === b.blending
&& a.alphaTest === b.alphaTest
&& a.depthTest === b.depthTest
&& a.depthWrite === b.depthWrite
;
}
/**
*
* @param {THREE.Material} a
* @returns {number}
*/
function materialChartingHash(a) {
const flags = (a.visible ? 1 : 0)
| ((a.transparent ? 1 : 0) << 1)
| ((a.depthTest ? 1 : 0) << 2)
| ((a.depthWrite ? 1 : 0) << 3)
| (([NormalBlending, AdditiveBlending, MultiplyBlending].indexOf(a.blending) & 0b11) << 4)
| ((Math.floor(a.alphaTest * 15)) << 6) // 4 bit
;
return flags;
}
/**
*
* @param {THREE.DataTexture} metalness
* @param {THREE.DataTexture} roughness
* @returns {THREE.DataTexture}
*/
function merge_textures_metalness_rougness({
metalness,
roughness
}) {
const ref = metalness !== undefined ? metalness : roughness;
const pixel_count = ref.image.width * ref.image.height;
const result_data = new Uint8Array(pixel_count * 4);
const result = new DataTexture(result_data, ref.image.width, ref.image.height);
result.encoding = ref.encoding;
if (metalness !== undefined) {
for (let i = 0; i < pixel_count; i++) {
result_data[i * 4 + 2] = metalness.image.data[i];
}
}
if (roughness !== undefined) {
for (let i = 0; i < pixel_count; i++) {
result_data[i * 4 + 1] = roughness.image.data[i];
}
}
result.needsUpdate = true;
return result;
}
/**
*
* @param {THREE.Material[]} source
* @param {Map<string, THREE.Texture>} textures
* @returns {Map<THREE.Material, THREE.Material>}
*/
function merge_materials(source, textures) {
// collect unique materials
const unique_materials = new HashMap({
keyEqualityFunction: materialsSimilarForCharting,
keyHashFunction: materialChartingHash
});
for (let i = 0; i < source.length; i++) {
const material = source[i];
let bucket = unique_materials.get(material);
if (bucket === undefined) {
bucket = [];
unique_materials.set(material, bucket);
}
bucket.push(material);
}
// build new materials
const source_unique_keys = [];
collectIteratorValueToArray(source_unique_keys, unique_materials.keys());
/**
*
* @type {Map<THREE.Material, THREE.Material>}
*/
const result = new Map();
for (let i = 0; i < source_unique_keys.length; i++) {
const source_material = source_unique_keys[i];
const result_material = new MeshStandardMaterial();
result_material.name = `Generated charted material ${i}`;
result_material.visible = source_material.visible;
result_material.transparent = source_material.transparent;
result_material.blending = source_material.blending;
result_material.alphaTest = source_material.alphaTest;
result_material.depthTest = source_material.depthTest;
result_material.depthWrite = source_material.depthWrite;
// write atlased texture
const texture_attachments = TextureAttachmentsByMaterialType[result_material.type];
for (let j = 0; j < texture_attachments.length; j++) {
const textureAttachment = texture_attachments[j];
const existing_texture = textures.get(textureAttachment.name);
if (existing_texture !== undefined) {
textureAttachment.write(result_material, existing_texture);
}
}
// write result mapping
const destination_materials = unique_materials.get(source_material);
for (let j = 0; j < destination_materials.length; j++) {
result.set(destination_materials[j], result_material);
}
}
return result;
}
/**
*
* @param {Sampler2D} destination
* @param {Sampler2D} source
* @param {string} texture_name
*/
function extract_texture_data(destination, source, texture_name) {
switch (texture_name) {
case 'ao':
sampler2d_copy_channel_data(source, 0, destination, 0);
break;
case 'roughness':
sampler2d_copy_channel_data(source, 1, destination, 0);
break;
case 'metalness':
sampler2d_copy_channel_data(source, 2, destination, 0);
break;
default:
sampler2d_transfer_data(source, destination);
}
}
/**
*
* @param {number[]} destination
* @param {THREE.Material|THREE.MeshStandardMaterial} source_material
* @param {string} texture_name
*/
function compute_default_texture_texel_value(destination, source_material, texture_name) {
switch (texture_name) {
case 'diffuse':
destination[0] = 255;
destination[1] = 255;
destination[2] = 255;
if (destination.length > 3) {
destination[3] = 255;
}
break;
case 'metalness':
for (let i = 0; i < destination.length; i++) {
destination[i] = Math.round(source_material.metalness * 255);
}
if (destination.length > 3) {
destination[3] = 255;
}
break;
case 'roughness':
for (let i = 0; i < destination.length; i++) {
destination[i] = Math.round(source_material.roughness * 255);
}
if (destination.length > 3) {
destination[3] = 255;
}
break;
case 'ao':
for (let i = 0; i < destination.length; i++) {
destination[i] = 255;
}
break;
case 'normal':
destination[0] = 127;
destination[1] = 127;
destination[2] = 255;
break;
}
}
/**
*
* @param {Sampler2D} sampler
* @param {THREE.MeshStandardMaterial} material
* @param {string} texture_name
*/
function apply_material_shading_to_texture(sampler, material, texture_name) {
switch (texture_name) {
case 'diffuse': {
const r = material.color.r;
const g = material.color.g;
const b = material.color.b;
const opacity = material.opacity;
const itemSize = sampler.itemSize;
const data = sampler.data;
const data_size = data.length;
if (r < 1 || g < 1 || b < 1) {
// apply color tint
for (let i = 0; i < data_size; i += itemSize) {
data[i] *= r;
data[i + 1] *= g;
data[i + 2] *= b;
}
}
if (itemSize > 3 && opacity < 1) {
// apply opacity
for (let i = 3; i < data_size; i += itemSize) {
data[i] *= opacity;
}
}
}
break;
default:
// do nothing
}
}
/**
*
* @param {Sampler2D} destination
* @param {THREE.Material} source_material
* @param {string} texture_name
*/
function compute_default_texture_value(destination, source_material, texture_name) {
const itemSize = destination.itemSize;
const texel = new Array(itemSize);
compute_default_texture_texel_value(texel, source_material, texture_name);
const width = destination.width;
const height = destination.height;
const destination_data = destination.data;
if (itemSize === 1) {
// special fast case
destination_data.fill(texel[0]);
} else {
const texel_count = width * height;
for (let i = 0; i < texel_count; i++) {
const destination_offset = i * itemSize;
destination_data.set(texel, destination_offset);
}
}
}
/**
*
* @param {string} texture_name
* @returns {{type:BinaryDataType,itemSize:number, default_value:[]}}
*/
function build_atlas_sampler_options_texture_name(texture_name) {
let type;
let itemSize;
let default_value;
switch (texture_name) {
case 'normal':
type = BinaryDataType.Uint8;
itemSize = 3;
default_value = [127, 127, 255];
break;
case 'ao':
type = BinaryDataType.Uint8;
itemSize = 1;
default_value = [255];
break;
case "metalness":
type = BinaryDataType.Uint8;
itemSize = 1;
default_value = [0];
break;
case "roughness":
type = BinaryDataType.Uint8;
itemSize = 1;
default_value = [255];
break;
default:
type = BinaryDataType.Uint8;
itemSize = 4;
default_value = [255, 255, 255, 255];
break;
}
return {
type,
itemSize,
default_value
};
}
export class MaterialOptimizationContext {
constructor() {
/**
*
* @type {THREE.Texture[]}
*/
this.textures = [];
/**
*
* @type {THREE.Mesh[]}
*/
this.meshes = [];
/**
*
* @type {Map<string,Sampler2D>}
*/
this.atlases = new Map();
/**
*
* @type {Vector2[]}
*/
this.atlas_size = [];
}
/**
*
* @param {MaterialDescriptor[]} materials
*/
pack_atlas(materials) {
const uv_indices = [];
// figure out UV sets
materials.forEach(m => {
for (let i = 0; i < m.uvs.length; i++) {
if (m.uvs[i] !== undefined) {
array_push_if_unique(uv_indices, i);
}
}
});
for (let i = 0; i < uv_indices.length; i++) {
const uv_index = uv_indices[i];
const packer = new MaxRectanglesPacker(1, 1);
let pow_x = 0;
let pow_y = 0;
const uv_relevant_materials = materials.filter(d => d.uvs[uv_index] !== undefined);
const rectangles = uv_relevant_materials.map(d => {
const uv_context = d.uvs[uv_index];
const resolution = uv_context.resolution;
return new AABB2(0, 0, resolution.x + ATLAS_PATCH_BORDER * 2, resolution.y + ATLAS_PATCH_BORDER * 2);
});
let cycle_index = 0;
while (!packer.addMany(rectangles)) {
cycle_index++;
if (cycle_index % 2 !== 0) {
pow_x++;
} else {
pow_y++;
}
const size_x = Math.pow(2, pow_x);
const size_y = Math.pow(2, pow_y);
const max_texture_size = MaterialOptimizationContext.MAX_TEXTURE_SIZE;
if (size_x > max_texture_size || size_y > max_texture_size) {
throw new Error(`Maximum texture size exceeded '${max_texture_size}'`);
}
packer.resize(size_x, size_y);
}
this.atlas_size[uv_index] = packer.size.clone();
// all packed, write back bounds
rectangles.forEach((r, i) => {
const materialDescriptor = uv_relevant_materials[i];
const p = materialDescriptor.uvs[uv_index].atlas_patch;
p.set(r.x0 + ATLAS_PATCH_BORDER, r.y0 + ATLAS_PATCH_BORDER, r.x1 - ATLAS_PATCH_BORDER, r.y1 - ATLAS_PATCH_BORDER);
});
}
}
/**
*
* @param {MaterialDescriptor[]} materials
*/
build_atlas_samplers(materials) {
let i;
this.atlases.clear();
const names = [];
const uv_indices = [];
const material_count = materials.length;
for (i = 0; i < material_count; i++) {
const material = materials[i];
const source_material = material.source_material;
const materialType = source_material.type;
/**
*
* @type {TextureAttachment[]}
*/
const attachments = TextureAttachmentsByMaterialType[materialType];
for (let i = 0; i < attachments.length; i++) {
const attachment = attachments[i];
const attachment_texture = attachment.read(source_material);
if (!attachment_texture) {
// no attachment on the material
continue;
}
if (array_push_if_unique(names, attachment.name)) {
// added
uv_indices[names.length - 1] = attachment.uv_index;
}
}
}
for (i = 0; i < names.length; i++) {
const name = names[i];
const uv_index = uv_indices[i];
const size = this.atlas_size[uv_index];
const width = size.x;
const height = size.y;
const sampler_options = build_atlas_sampler_options_texture_name(name);
const TA = compute_typed_array_constructor_from_data_type(sampler_options.type);
const itemSize = sampler_options.itemSize;
const sampler_data_size = itemSize * width * height;
const sampler_data = new TA(sampler_data_size);
for (let j = 0; j < sampler_data_size; j += itemSize) {
sampler_data.set(sampler_options.default_value, j);
}
const atlas_sampler = new Sampler2D(sampler_data, itemSize, width, height);
this.atlases.set(name, atlas_sampler);
}
}
/**
*
* @param {MaterialDescriptor} material
*/
write_material_to_atlas(material) {
//patch textures
const source_material = material.source_material;
const materialType = source_material.type;
/**
*
* @type {TextureAttachment[]}
*/
const attachments = TextureAttachmentsByMaterialType[materialType];
const attachment_names = Array.from(this.atlases.keys());
for (let i = 0; i < attachments.length; i++) {
const attachment = attachments[i];
const texture_name = attachment.name;
if (!attachment_names.includes(texture_name)) {
continue;
}
const uv_context = material.uvs[attachment.uv_index];
if (uv_context === undefined) {
// material does not participate in this UV set
continue;
}
const atlas_sampler = this.atlases.get(texture_name);
let source_sampler;
const texture = attachment.read(source_material);
const resolution = uv_context.resolution;
const texture_width = resolution.x;
const texture_height = resolution.y;
if (texture !== undefined && texture !== null) {
// source_sampler = Sampler2D.uint8(5, texture_width, texture_height);
//
// source_sampler.fill(0, 0, texture_width, texture_height, [255, 255, 255, 255]);
source_sampler = convertTexture2Sampler2D(texture, texture_width, texture_height, false);
if (source_sampler.itemSize !== atlas_sampler.itemSize) {
const destination = Sampler2D.uint8(atlas_sampler.itemSize, texture_width, texture_height);
extract_texture_data(destination, source_sampler, texture_name);
source_sampler = destination;
}
} else {
// texture for a given slot is missing, create a default
source_sampler = Sampler2D.uint8(atlas_sampler.itemSize, texture_width, texture_height);
compute_default_texture_value(source_sampler, source_material, texture_name);
}
// apply modification to texture to remove dependency on source material
apply_material_shading_to_texture(source_sampler, source_material, texture_name);
const patch = uv_context.atlas_patch;
sampler2d_sub_copy_same_item_size(
atlas_sampler,
source_sampler,
0, 0,
patch.x0, patch.y0, patch.getWidth(), patch.getHeight()
);
}
}
/**
*
* @param {THREE.Mesh} mesh
*/
addMesh(mesh) {
const material = mesh.material;
if (material.isMeshStandardMaterial) {
this.meshes.push(mesh);
} else {
// throw new Error(`Unsupported material type '${material.type}'`);
}
}
/**
*
* @param {THREE.Object} o
*/
addObject(o) {
traverseThreeObject(o, (o) => {
if (o.isMesh) {
this.addMesh(o);
}
});
}
/**
* Update materials and geometries
*/
update() {
// collect unique materials and their geometries
/**
*
* @type {Map<MaterialDescriptor, THREE.BufferGeometry[]>}
*/
const material_to_geometry_map = new Map();
/**
*
* @type {Map<MaterialDescriptor, THREE.Mesh[]>}
*/
const material_to_mesh_map = new Map();
const unique_materials = new HashMap({
keyHashFunction: computeMaterialHash,
keyEqualityFunction: computeMaterialEquality
});
const input_meshes = this.meshes;
const input_mesh_count = input_meshes.length;
for (let i = 0; i < input_mesh_count; i++) {
const mesh = input_meshes[i];
if (!is_compliant_mesh(mesh)) {
continue;
}
const material = mesh.material;
let material_descriptor = unique_materials.get(material);
if (material_descriptor === undefined) {
material_descriptor = new MaterialDescriptor();
material_descriptor.source_material = material;
material_descriptor.build_uvs();
unique_materials.set(material, material_descriptor);
material_to_geometry_map.set(material_descriptor, []);
material_to_mesh_map.set(material_descriptor, []);
}
const geometries = material_to_geometry_map.get(material_descriptor);
array_push_if_unique(geometries, mesh.geometry);
const mesh_bucket = material_to_mesh_map.get(material_descriptor);
mesh_bucket.push(mesh);
}
// collect materials
/**
*
* @type {MaterialDescriptor[]}
*/
const unique_material_array = [];
collectIteratorValueToArray(unique_material_array, unique_materials.values());
const unique_material_count = unique_material_array.length;
if (unique_material_count <= 4) {
// too few unique materials, no point in atlasing
return;
}
this.pack_atlas(unique_material_array);
this.build_atlas_samplers(unique_material_array);
const textures = this.initialize_atlas_textures();
for (let i = 0; i < unique_material_count; i++) {
const mat = unique_material_array[i];
this.write_material_to_atlas(mat);
// get all associated geometries
const geometries = material_to_geometry_map.get(mat);
for (let j = 0; j < geometries.length; j++) {
const bufferGeometry = geometries[j];
this.apply_geometry_uv_changes(bufferGeometry, mat);
}
}
/*
merge roughness and metalness. Three.js shaders sample roughness from G channel (index = 1) and metalness from B channel (index 2)
Because of this, we can't pass B/W textures with only Red channel set, so we are forced to create a merged texture from metalness and roughnes
*/
if (textures.get('roughness') !== undefined || textures.get('metalness') !== undefined) {
const result_texture = merge_textures_metalness_rougness({
roughness: textures.get('roughness'),
metalness: textures.get('metalness')
});
if (textures.get('roughness') !== undefined) {
textures.set('roughness', result_texture);
}
if (textures.get('metalness') !== undefined) {
textures.set('roughness', result_texture);
}
}
// update textures
for (const [name, texture] of textures) {
texture.needsUpdate = true;
}
// compute merged materials
const unique_source_materials = [];
collectIteratorValueToArray(unique_source_materials, unique_materials.keys());
const merged_materials = merge_materials(unique_source_materials, textures);
for (let i = 0; i < unique_material_count; i++) {
const mat = unique_material_array[i];
// bind material
const new_material = merged_materials.get(mat.source_material);
// set new material to all associated meshes
const meshes = material_to_mesh_map.get(mat);
for (let j = 0; j < meshes.length; j++) {
const mesh = meshes[j];
mesh.material = new_material;
}
}
}
/**
*
* @return {Map<string, THREE.DataTexture>}
*/
initialize_atlas_textures() {
const textures = new Map();
//build new textures
for (const [name, sampler] of this.atlases) {
const sampler_data_type = compute_binary_data_type_from_typed_array(sampler.data);
const format = channelCountToThreeTextureFormat(sampler.itemSize);
const type = computeThreeTextureTypeFromDataType(sampler_data_type);
const texture = new DataTexture(sampler.data, sampler.width, sampler.height, format, type);
texture.wrapS = ClampToEdgeWrapping;
texture.wrapT = ClampToEdgeWrapping;
texture.name = name;
texture.internalFormat = normalized_internal_format(sampler_data_type, sampler.itemSize);
prepare_atlas_texture(texture, name);
textures.set(name, texture);
}
return textures;
}
/**
*
* @param {THREE.BufferGeometry} bufferGeometry
* @param {MaterialDescriptor} mat
*/
apply_geometry_uv_changes(bufferGeometry, mat) {
const uv_count = BUFFER_GEOMETRY_UVS.length;
for (let k = 0; k < uv_count; k++) {
const uv_metadata = BUFFER_GEOMETRY_UVS[k];
// update UVs
const uv_attribute = bufferGeometry.attributes[uv_metadata.name];
if (uv_attribute === undefined) {
// no UVs, nothing to update
continue;
}
const uv_index = uv_metadata.index;
const uv_context = mat.uvs[uv_index];
if (uv_context === undefined) {
// no UV context present
// TODO need to force context to exist in this case or remove the attribute
continue;
}
const atlas_size = this.atlas_size[uv_index];
const atlas_width = atlas_size.x;
const atlas_height = atlas_size.y;
const patch = uv_context.atlas_patch;
const matrix3 = new Matrix3();
matrix3.setUvTransform(
patch.x0 / atlas_width,
patch.y0 / atlas_height,
patch.getWidth() / atlas_width,
patch.getHeight() / atlas_height,
0,
0, 0
);
uv_attribute.applyMatrix3(matrix3);
uv_attribute.needsUpdate = true;
}
}
}
/**
*
* @type {number}
*/
MaterialOptimizationContext.MAX_TEXTURE_SIZE = 4096;