pex-renderer
Version:
Physically Based Renderer (PBR) and scene graph designed as ECS for PEX: define entities to be rendered as collections of components with their update orchestrated by systems.
222 lines (191 loc) • 6.87 kB
JavaScript
import { aabb } from "pex-geom";
import { vec3 } from "pex-math";
import { NAMESPACE, TEMP_AABB } from "../utils.js";
const attributeMap = {
aPosition: "positions",
aNormal: "normals",
aTangent: "tangents",
aVertexColor: "vertexColors",
aTexCoord0: ["uvs", "texCoords", "uvs0", "texCoords0"],
aTexCoord1: ["uvs1", "texCoords1"],
aWeight: "weights",
aJoint: "joints",
aOffset: "offsets",
aScale: "scales",
aRotation: "rotations",
aColor: "colors",
};
const attributeMapKeys = Object.keys(attributeMap);
const instancedAttributes = ["aOffset", "aScale", "aRotation", "aColor"];
const indicesProps = ["cells", "indices"];
/**
* Geometry system
*
* Adds:
* - "bounds" to geometry components
* - "dirty" to geometry components properties
* - "_geometry" to entities as reference to internal cache
* @param {import("../types.js").SystemOptions} options
* @returns {import("../types.js").System}
* @alias module:systems.geometry
*/
export default ({ ctx }) => ({
type: "geometry-system",
cache: {},
debug: false,
updateBounds(geometry) {
const positions = geometry.positions.data || geometry.positions;
const offsets = geometry.offsets?.data || geometry.offsets;
geometry.bounds ||= aabb.create();
// TODO: handle skin system?
if (offsets?.length) {
aabb.fromPoints(geometry.bounds, offsets);
aabb.fromPoints(TEMP_AABB, positions);
vec3.add(geometry.bounds[0], TEMP_AABB[0]);
vec3.add(geometry.bounds[1], TEMP_AABB[1]);
} else {
aabb.fromPoints(geometry.bounds, positions);
}
geometry.bounds.dirty = false;
},
updateGeometryEntity(entity) {
const geometry = entity.geometry;
this.cache[entity.id] ||= { geometry: null, attributes: {} };
if (this.debug && !this.cache[entity.id].geometry) {
console.debug(
NAMESPACE,
this.type,
"add to cache",
entity.id,
this.cache[entity.id],
);
}
const cachedGeom = this.cache[entity.id];
const geometryDirty = cachedGeom.geometry !== geometry;
// Cache geometry properties
if (geometryDirty) {
if (this.debug) {
console.debug(NAMESPACE, this.type, "update", entity.id, geometry);
}
cachedGeom.geometry = geometry;
cachedGeom.instances = geometry.instances;
cachedGeom.count = geometry.count;
cachedGeom.primitive = geometry.primitive;
// Add custom attributes
if (cachedGeom.customAttributes) {
for (let i = 0; i < cachedGeom.customAttributes.length; i++) {
const attributeName = cachedGeom.customAttributes[i];
if (!geometry.attributes || !geometry.attributes[attributeName]) {
ctx.dispose(
cachedGeom.attributes[attributeName].buffer ||
cachedGeom.attributes[attributeName],
);
delete cachedGeom.attributes[attributeName];
}
}
}
if (geometry.attributes) {
Object.assign(cachedGeom.attributes, geometry.attributes);
cachedGeom.customAttributes = Object.keys(geometry.attributes);
} else {
cachedGeom.customAttributes = [];
}
}
// Add index buffer
for (let i = 0; i < indicesProps.length; i++) {
const indicesValue = geometry[indicesProps[i]];
if (indicesValue) {
if (!(geometryDirty || indicesValue.dirty)) continue;
indicesValue.dirty = false;
if (indicesValue.buffer?.class === "indexBuffer") {
cachedGeom.indices = indicesValue;
} else {
cachedGeom.indices ||= ctx.indexBuffer([[1, 1, 1]]);
ctx.update(cachedGeom.indices, {
data: indicesValue.data || indicesValue,
});
// TODO: why not passing this to ctx.update?
//TODO: check if mutating indexBuffer here is ok
cachedGeom.indices.offset = indicesValue.offset;
}
}
}
let boundsDirty = !geometry.bounds || geometry.bounds.dirty;
// Add vertex buffers
for (let i = 0; i < attributeMapKeys.length; i++) {
const attributeName = attributeMapKeys[i];
const attributeValue =
geometry[
Array.isArray(attributeMap[attributeName])
? attributeMap[attributeName].find((prop) => geometry[prop])
: attributeMap[attributeName]
];
if (attributeValue) {
if (!(geometryDirty || attributeValue.dirty)) continue;
attributeValue.dirty = false;
const data = attributeValue.data || attributeValue; //.data should be deprecated
// Set the attribute
if (attributeValue.buffer?.class === "vertexBuffer") {
cachedGeom.attributes[attributeName] = attributeValue;
} else {
cachedGeom.attributes[attributeName] ||= {
buffer: ctx.vertexBuffer([[1, 1, 1]]),
};
const attribute = cachedGeom.attributes[attributeName];
ctx.update(attribute.buffer, { data });
// TODO: why not passing this to ctx.update?
attribute.offset = attributeValue.offset;
attribute.stride = attributeValue.stride;
attribute.divisor =
attributeValue.divisor ||
(instancedAttributes.includes(attributeName) ? 1 : undefined);
}
} else if (cachedGeom.attributes[attributeName]) {
ctx.dispose(
cachedGeom.attributes[attributeName].buffer ||
cachedGeom.attributes[attributeName],
);
delete cachedGeom.attributes[attributeName];
}
}
// Compute the bounds
if (boundsDirty) this.updateBounds(geometry);
},
//TODO: should geometry components have their own id?
//TODO: Use transducers
//https://gist.github.com/craigdallimore/8b5b9d9e445bfa1e383c569e458c3e26
update(entities) {
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
if (entity.geometry) {
try {
this.updateGeometryEntity(entity);
entity._geometry = this.cache[entity.id];
} catch (error) {
console.error(NAMESPACE, this.type, "update failed", error, entity);
}
}
}
},
dispose(entities) {
if (entities) {
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
if (entity._geometry) {
if (entity._geometry.indices) {
const resource =
entity._geometry.indices.buffer || entity._geometry.indices;
if (resource.handle) ctx.dispose(resource);
}
for (let attribute of Object.values(entity._geometry.attributes)) {
const resource = attribute.buffer || attribute;
if (resource.handle) ctx.dispose(resource);
}
delete this.cache[entity.id];
}
}
} else {
this.cache = {};
}
},
});