@openhps/core
Version:
Open Hybrid Positioning System - Core component
647 lines (591 loc) • 18.2 kB
JavaScript
import { hash, hashString } from '../../nodes/core/NodeUtils.js';
let _id = 0;
function getKeys(obj) {
const keys = Object.keys(obj);
let proto = Object.getPrototypeOf(obj);
while (proto) {
const descriptors = Object.getOwnPropertyDescriptors(proto);
for (const key in descriptors) {
if (descriptors[key] !== undefined) {
const descriptor = descriptors[key];
if (descriptor && typeof descriptor.get === 'function') {
keys.push(key);
}
}
}
proto = Object.getPrototypeOf(proto);
}
return keys;
}
/**
* A render object is the renderer's representation of single entity that gets drawn
* with a draw command. There is no unique mapping of render objects to 3D objects in the
* scene since render objects also depend from the used material, the current render context
* and the current scene's lighting.
*
* In general, the basic process of the renderer is:
*
* - Analyze the 3D objects in the scene and generate render lists containing render items.
* - Process the render lists by calling one or more render commands for each render item.
* - For each render command, request a render object and perform the draw.
*
* The module provides an interface to get data required for the draw command like the actual
* draw parameters or vertex buffers. It also holds a series of caching related methods since
* creating render objects should only be done when necessary.
*
* @private
*/
class RenderObject {
/**
* Constructs a new render object.
*
* @param {Nodes} nodes - Renderer component for managing nodes related logic.
* @param {Geometries} geometries - Renderer component for managing geometries.
* @param {Renderer} renderer - The renderer.
* @param {Object3D} object - The 3D object.
* @param {Material} material - The 3D object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {LightsNode} lightsNode - The lights node.
* @param {RenderContext} renderContext - The render context.
* @param {ClippingContext} clippingContext - The clipping context.
*/
constructor(nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext) {
this.id = _id++;
/**
* Renderer component for managing nodes related logic.
*
* @type {Nodes}
* @private
*/
this._nodes = nodes;
/**
* Renderer component for managing geometries.
*
* @type {Geometries}
* @private
*/
this._geometries = geometries;
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* The 3D object.
*
* @type {Object3D}
*/
this.object = object;
/**
* The 3D object's material.
*
* @type {Material}
*/
this.material = material;
/**
* The scene the 3D object belongs to.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera the 3D object should be rendered with.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The lights node.
*
* @type {LightsNode}
*/
this.lightsNode = lightsNode;
/**
* The render context.
*
* @type {RenderContext}
*/
this.context = renderContext;
/**
* The 3D object's geometry.
*
* @type {BufferGeometry}
*/
this.geometry = object.geometry;
/**
* The render object's version.
*
* @type {number}
*/
this.version = material.version;
/**
* The draw range of the geometry.
*
* @type {?Object}
* @default null
*/
this.drawRange = null;
/**
* An array holding the buffer attributes
* of the render object. This entails attribute
* definitions on geometry and node level.
*
* @type {?Array<BufferAttribute>}
* @default null
*/
this.attributes = null;
/**
* A reference to a render pipeline the render
* object is processed with.
*
* @type {RenderPipeline}
* @default null
*/
this.pipeline = null;
/**
* Only relevant for objects using
* multiple materials. This represents a group entry
* from the respective `BufferGeometry`.
*
* @type {?{start: number, count: number}}
* @default null
*/
this.group = null;
/**
* An array holding the vertex buffers which can
* be buffer attributes but also interleaved buffers.
*
* @type {?Array<BufferAttribute|InterleavedBuffer>}
* @default null
*/
this.vertexBuffers = null;
/**
* The parameters for the draw command.
*
* @type {?Object}
* @default null
*/
this.drawParams = null;
/**
* If this render object is used inside a render bundle,
* this property points to the respective bundle group.
*
* @type {?BundleGroup}
* @default null
*/
this.bundle = null;
/**
* The clipping context.
*
* @type {ClippingContext}
*/
this.clippingContext = clippingContext;
/**
* The clipping context's cache key.
*
* @type {string}
*/
this.clippingContextCacheKey = clippingContext !== null ? clippingContext.cacheKey : '';
/**
* The initial node cache key.
*
* @type {number}
*/
this.initialNodesCacheKey = this.getDynamicCacheKey();
/**
* The initial cache key.
*
* @type {number}
*/
this.initialCacheKey = this.getCacheKey();
/**
* The node builder state.
*
* @type {?NodeBuilderState}
* @private
* @default null
*/
this._nodeBuilderState = null;
/**
* An array of bindings.
*
* @type {?Array<BindGroup>}
* @private
* @default null
*/
this._bindings = null;
/**
* Reference to the node material observer.
*
* @type {?NodeMaterialObserver}
* @private
* @default null
*/
this._monitor = null;
/**
* An event listener which is defined by `RenderObjects`. It performs
* clean up tasks when `dispose()` on this render object.
*
* @method
*/
this.onDispose = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderObject = true;
/**
* An event listener which is executed when `dispose()` is called on
* the render object's material.
*
* @method
*/
this.onMaterialDispose = () => {
this.dispose();
};
this.material.addEventListener('dispose', this.onMaterialDispose);
}
/**
* Updates the clipping context.
*
* @param {ClippingContext} context - The clipping context to set.
*/
updateClipping(context) {
this.clippingContext = context;
}
/**
* Whether the clipping requires an update or not.
*
* @type {boolean}
* @readonly
*/
get clippingNeedsUpdate() {
if (this.clippingContext === null || this.clippingContext.cacheKey === this.clippingContextCacheKey) return false;
this.clippingContextCacheKey = this.clippingContext.cacheKey;
return true;
}
/**
* The number of clipping planes defined in context of hardware clipping.
*
* @type {number}
* @readonly
*/
get hardwareClippingPlanes() {
return this.material.hardwareClipping === true ? this.clippingContext.unionClippingCount : 0;
}
/**
* Returns the node builder state of this render object.
*
* @return {NodeBuilderState} The node builder state.
*/
getNodeBuilderState() {
return this._nodeBuilderState || (this._nodeBuilderState = this._nodes.getForRender(this));
}
/**
* Returns the node material observer of this render object.
*
* @return {NodeMaterialObserver} The node material observer.
*/
getMonitor() {
return this._monitor || (this._monitor = this.getNodeBuilderState().observer);
}
/**
* Returns an array of bind groups of this render object.
*
* @return {Array<BindGroup>} The bindings.
*/
getBindings() {
return this._bindings || (this._bindings = this.getNodeBuilderState().createBindings());
}
/**
* Returns a binding group by group name of this render object.
*
* @param {string} name - The name of the binding group.
* @return {?BindGroup} The bindings.
*/
getBindingGroup(name) {
for (const bindingGroup of this.getBindings()) {
if (bindingGroup.name === name) {
return bindingGroup;
}
}
}
/**
* Returns the index of the render object's geometry.
*
* @return {?BufferAttribute} The index. Returns `null` for non-indexed geometries.
*/
getIndex() {
return this._geometries.getIndex(this);
}
/**
* Returns the indirect buffer attribute.
*
* @return {?BufferAttribute} The indirect attribute. `null` if no indirect drawing is used.
*/
getIndirect() {
return this._geometries.getIndirect(this);
}
/**
* Returns an array that acts as a key for identifying the render object in a chain map.
*
* @return {Array<Object>} An array with object references.
*/
getChainArray() {
return [this.object, this.material, this.context, this.lightsNode];
}
/**
* This method is used when the geometry of a 3D object has been exchanged and the
* respective render object now requires an update.
*
* @param {BufferGeometry} geometry - The geometry to set.
*/
setGeometry(geometry) {
this.geometry = geometry;
this.attributes = null;
}
/**
* Returns the buffer attributes of the render object. The returned array holds
* attribute definitions on geometry and node level.
*
* @return {Array<BufferAttribute>} An array with buffer attributes.
*/
getAttributes() {
if (this.attributes !== null) return this.attributes;
const nodeAttributes = this.getNodeBuilderState().nodeAttributes;
const geometry = this.geometry;
const attributes = [];
const vertexBuffers = new Set();
for (const nodeAttribute of nodeAttributes) {
const attribute = nodeAttribute.node && nodeAttribute.node.attribute ? nodeAttribute.node.attribute : geometry.getAttribute(nodeAttribute.name);
if (attribute === undefined) continue;
attributes.push(attribute);
const bufferAttribute = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
vertexBuffers.add(bufferAttribute);
}
this.attributes = attributes;
this.vertexBuffers = Array.from(vertexBuffers.values());
return attributes;
}
/**
* Returns the vertex buffers of the render object.
*
* @return {Array<BufferAttribute|InterleavedBuffer>} An array with buffer attribute or interleaved buffers.
*/
getVertexBuffers() {
if (this.vertexBuffers === null) this.getAttributes();
return this.vertexBuffers;
}
/**
* Returns the draw parameters for the render object.
*
* @return {?{vertexCount: number, firstVertex: number, instanceCount: number, firstInstance: number}} The draw parameters.
*/
getDrawParameters() {
const {
object,
material,
geometry,
group,
drawRange
} = this;
const drawParams = this.drawParams || (this.drawParams = {
vertexCount: 0,
firstVertex: 0,
instanceCount: 0,
firstInstance: 0
});
const index = this.getIndex();
const hasIndex = index !== null;
const instanceCount = geometry.isInstancedBufferGeometry ? geometry.instanceCount : object.count > 1 ? object.count : 1;
if (instanceCount === 0) return null;
drawParams.instanceCount = instanceCount;
if (object.isBatchedMesh === true) return drawParams;
let rangeFactor = 1;
if (material.wireframe === true && !object.isPoints && !object.isLineSegments && !object.isLine && !object.isLineLoop) {
rangeFactor = 2;
}
let firstVertex = drawRange.start * rangeFactor;
let lastVertex = (drawRange.start + drawRange.count) * rangeFactor;
if (group !== null) {
firstVertex = Math.max(firstVertex, group.start * rangeFactor);
lastVertex = Math.min(lastVertex, (group.start + group.count) * rangeFactor);
}
const position = geometry.attributes.position;
let itemCount = Infinity;
if (hasIndex) {
itemCount = index.count;
} else if (position !== undefined && position !== null) {
itemCount = position.count;
}
firstVertex = Math.max(firstVertex, 0);
lastVertex = Math.min(lastVertex, itemCount);
const count = lastVertex - firstVertex;
if (count < 0 || count === Infinity) return null;
drawParams.vertexCount = count;
drawParams.firstVertex = firstVertex;
return drawParams;
}
/**
* Returns the render object's geometry cache key.
*
* The geometry cache key is part of the material cache key.
*
* @return {string} The geometry cache key.
*/
getGeometryCacheKey() {
const {
geometry
} = this;
let cacheKey = '';
for (const name of Object.keys(geometry.attributes).sort()) {
const attribute = geometry.attributes[name];
cacheKey += name + ',';
if (attribute.data) cacheKey += attribute.data.stride + ',';
if (attribute.offset) cacheKey += attribute.offset + ',';
if (attribute.itemSize) cacheKey += attribute.itemSize + ',';
if (attribute.normalized) cacheKey += 'n,';
}
// structural equality isn't sufficient for morph targets since the
// data are maintained in textures. only if the targets are all equal
// the texture and thus the instance of `MorphNode` can be shared.
for (const name of Object.keys(geometry.morphAttributes).sort()) {
const targets = geometry.morphAttributes[name];
cacheKey += 'morph-' + name + ',';
for (let i = 0, l = targets.length; i < l; i++) {
const attribute = targets[i];
cacheKey += attribute.id + ',';
}
}
if (geometry.index) {
cacheKey += 'index,';
}
return cacheKey;
}
/**
* Returns the render object's material cache key.
*
* The material cache key is part of the render object cache key.
*
* @return {number} The material cache key.
*/
getMaterialCacheKey() {
const {
object,
material
} = this;
let cacheKey = material.customProgramCacheKey();
for (const property of getKeys(material)) {
if (/^(is[A-Z]|_)|^(visible|version|uuid|name|opacity|userData)$/.test(property)) continue;
const value = material[property];
let valueKey;
if (value !== null) {
// some material values require a formatting
const type = typeof value;
if (type === 'number') {
valueKey = value !== 0 ? '1' : '0'; // Convert to on/off, important for clearcoat, transmission, etc
} else if (type === 'object') {
valueKey = '{';
if (value.isTexture) {
valueKey += value.mapping;
}
valueKey += '}';
} else {
valueKey = String(value);
}
} else {
valueKey = String(value);
}
cacheKey += /*property + ':' +*/valueKey + ',';
}
cacheKey += this.clippingContextCacheKey + ',';
if (object.geometry) {
cacheKey += this.getGeometryCacheKey();
}
if (object.skeleton) {
cacheKey += object.skeleton.bones.length + ',';
}
if (object.isBatchedMesh) {
cacheKey += object._matricesTexture.uuid + ',';
if (object._colorsTexture !== null) {
cacheKey += object._colorsTexture.uuid + ',';
}
}
if (object.count > 1) {
// TODO: https://github.com/mrdoob/three.js/pull/29066#issuecomment-2269400850
cacheKey += object.uuid + ',';
}
cacheKey += object.receiveShadow + ',';
return hashString(cacheKey);
}
/**
* Whether the geometry requires an update or not.
*
* @type {boolean}
* @readonly
*/
get needsGeometryUpdate() {
return this.geometry.id !== this.object.geometry.id;
}
/**
* Whether the render object requires an update or not.
*
* Note: There are two distinct places where render objects are checked for an update.
*
* 1. In `RenderObjects.get()` which is executed when the render object is request. This
* method checks the `needsUpdate` flag and recreates the render object if necessary.
* 2. In `Renderer._renderObjectDirect()` right after getting the render object via
* `RenderObjects.get()`. The render object's NodeMaterialObserver is then used to detect
* a need for a refresh due to material, geometry or object related value changes.
*
* TODO: Investigate if it's possible to merge both steps so there is only a single place
* that performs the 'needsUpdate' check.
*
* @type {boolean}
* @readonly
*/
get needsUpdate() {
return /*this.object.static !== true &&*/this.initialNodesCacheKey !== this.getDynamicCacheKey() || this.clippingNeedsUpdate;
}
/**
* Returns the dynamic cache key which represents a key that is computed per draw command.
*
* @return {number} The cache key.
*/
getDynamicCacheKey() {
let cacheKey = 0;
// `Nodes.getCacheKey()` returns an environment cache key which is not relevant when
// the renderer is inside a shadow pass.
if (this.material.isShadowPassMaterial !== true) {
cacheKey = this._nodes.getCacheKey(this.scene, this.lightsNode);
}
if (this.camera.isArrayCamera) {
cacheKey = hash(cacheKey, this.camera.cameras.length);
}
if (this.object.receiveShadow) {
cacheKey = hash(cacheKey, 1);
}
return cacheKey;
}
/**
* Returns the render object's cache key.
*
* @return {number} The cache key.
*/
getCacheKey() {
return this.getMaterialCacheKey() + this.getDynamicCacheKey();
}
/**
* Frees internal resources.
*/
dispose() {
this.material.removeEventListener('dispose', this.onMaterialDispose);
this.onDispose();
}
}
export default RenderObject;