playcanvas
Version:
PlayCanvas WebGL game engine
948 lines (945 loc) • 44.1 kB
JavaScript
import { Debug } from '../core/debug.js';
import { RefCountedObject } from '../core/ref-counted-object.js';
import { Vec3 } from '../core/math/vec3.js';
import { BoundingBox } from '../core/shape/bounding-box.js';
import { SEMANTIC_TANGENT, SEMANTIC_BLENDINDICES, TYPE_UINT8, SEMANTIC_BLENDWEIGHT, SEMANTIC_POSITION, TYPE_UINT16, TYPE_INT16, TYPE_INT8, BUFFER_STATIC, BUFFER_DYNAMIC, TYPE_FLOAT32, SEMANTIC_NORMAL, SEMANTIC_TEXCOORD, SEMANTIC_COLOR, PRIMITIVE_TRIANGLES, INDEXFORMAT_UINT32, INDEXFORMAT_UINT16, PRIMITIVE_POINTS, typedArrayIndexFormats, PRIMITIVE_LINES } from '../platform/graphics/constants.js';
import { IndexBuffer } from '../platform/graphics/index-buffer.js';
import { VertexBuffer } from '../platform/graphics/vertex-buffer.js';
import { VertexFormat } from '../platform/graphics/vertex-format.js';
import { VertexIterator } from '../platform/graphics/vertex-iterator.js';
import { RENDERSTYLE_WIREFRAME, RENDERSTYLE_POINTS, RENDERSTYLE_SOLID } from './constants.js';
/**
* @import { Geometry } from './geometry/geometry.js'
* @import { GraphicsDevice } from '../platform/graphics/graphics-device.js'
* @import { Morph } from './morph.js'
* @import { Skin } from './skin.js'
*/ let id = 0;
// Helper class used to store vertex / index data streams and related properties, when mesh is programmatically modified
class GeometryData {
constructor(){
this.initDefaults();
}
initDefaults() {
// by default, existing mesh is updated but not recreated, until .clear function is called
this.recreate = false;
// usage for buffers
this.verticesUsage = BUFFER_STATIC;
this.indicesUsage = BUFFER_STATIC;
// vertex and index buffer allocated size (maximum number of vertices / indices that can be stored in those without the need to reallocate them)
this.maxVertices = 0;
this.maxIndices = 0;
// current number of vertices and indices in use
this.vertexCount = 0;
this.indexCount = 0;
// dirty flags representing what needs be updated
this.vertexStreamsUpdated = false;
this.indexStreamUpdated = false;
// dictionary of vertex streams that need to be updated, looked up by semantic
this.vertexStreamDictionary = {};
// index stream data that needs to be updated
this.indices = null;
}
// function called when vertex stream is requested to be updated, and validates / updates currently used vertex count
_changeVertexCount(count, semantic) {
// update vertex count and validate it with existing streams
if (!this.vertexCount) {
this.vertexCount = count;
} else {
Debug.assert(this.vertexCount === count, `Vertex stream ${semantic} has ${count} vertices, which does not match already set streams with ${this.vertexCount} vertices.`);
}
}
static{
// default counts for vertex components
this.DEFAULT_COMPONENTS_POSITION = 3;
}
static{
this.DEFAULT_COMPONENTS_NORMAL = 3;
}
static{
this.DEFAULT_COMPONENTS_UV = 2;
}
static{
this.DEFAULT_COMPONENTS_COLORS = 4;
}
}
// class storing information about single vertex data stream
class GeometryVertexStream {
constructor(data, componentCount, dataType, dataTypeNormalize, asInt){
this.data = data; // array of data
this.componentCount = componentCount; // number of components
this.dataType = dataType; // format of elements (pc.TYPE_FLOAT32 ..)
this.dataTypeNormalize = dataTypeNormalize; // normalize element (divide by 255)
this.asInt = asInt; // treat data as integer (WebGL2 and WebGPU only)
}
}
/**
* A graphical primitive. The mesh is defined by a {@link VertexBuffer} and an optional
* {@link IndexBuffer}. It also contains a primitive definition which controls the type of the
* primitive and the portion of the vertex or index buffer to use.
*
* ## Mesh APIs
* There are two ways a mesh can be generated or updated.
*
* ### Simple Mesh API
* {@link Mesh} class provides interfaces such as {@link Mesh#setPositions} and {@link Mesh#setUvs}
* that provide a simple way to provide vertex and index data for the Mesh, and hiding the
* complexity of creating the {@link VertexFormat}. This is the recommended interface to use.
*
* A simple example which creates a Mesh with 3 vertices, containing position coordinates only, to
* form a single triangle.
*
* ```javascript
* const mesh = new pc.Mesh(device);
* const positions = [
* 0, 0, 0, // pos 0
* 1, 0, 0, // pos 1
* 1, 1, 0 // pos 2
* ];
* mesh.setPositions(positions);
* mesh.update();
* ```
*
* An example which creates a Mesh with 4 vertices, containing position and uv coordinates in
* channel 0, and an index buffer to form two triangles. Float32Array is used for positions and uvs.
*
* ```javascript
* const mesh = new pc.Mesh(device);
* const positions = new Float32Array([
* 0, 0, 0, // pos 0
* 1, 0, 0, // pos 1
* 1, 1, 0, // pos 2
* 0, 1, 0 // pos 3
* ]);
* const uvs = new Float32Array([
* 0, 1 // uv 3
* 1, 1, // uv 2
* 1, 0, // uv 1
* 0, 0, // uv 0
* ]);
* const indices = [
* 0, 1, 2, // triangle 0
* 0, 2, 3 // triangle 1
* ];
* mesh.setPositions(positions);
* mesh.setNormals(pc.calculateNormals(positions, indices));
* mesh.setUvs(0, uvs);
* mesh.setIndices(indices);
* mesh.update();
* ```
*
* This example demonstrates that vertex attributes such as position and normals, and also indices
* can be provided using Arrays ([]) and also Typed Arrays (Float32Array and similar). Note that
* typed arrays have higher performance, and are generally recommended for per-frame operations or
* larger meshes, but their construction using new operator is costly operation. If you only need
* to operate on a small number of vertices or indices, consider using Arrays to avoid the overhead
* associated with allocating Typed Arrays.
*
* Follow these links for more complex examples showing the functionality.
*
* - {@link https://playcanvas.github.io/#graphics/mesh-decals}
* - {@link https://playcanvas.github.io/#graphics/mesh-deformation}
* - {@link https://playcanvas.github.io/#graphics/mesh-generation}
* - {@link https://playcanvas.github.io/#graphics/point-cloud-simulation}
*
* ### Update Vertex and Index buffers
* This allows greater flexibility, but is more complex to use. It allows more advanced setups, for
* example sharing a Vertex or Index Buffer between multiple meshes. See {@link VertexBuffer},
* {@link IndexBuffer} and {@link VertexFormat} for details.
*
* @category Graphics
*/ class Mesh extends RefCountedObject {
/**
* Create a new Mesh instance.
*
* @param {GraphicsDevice} graphicsDevice - The graphics device used to manage this mesh.
* @param {object} [options] - Object for passing optional arguments.
* @param {boolean} [options.storageVertex] - Defines if the vertex buffer can be used as
* a storage buffer by a compute shader. Defaults to false. Only supported on WebGPU.
* @param {boolean} [options.storageIndex] - Defines if the index buffer can be used as
* a storage buffer by a compute shader. Defaults to false. Only supported on WebGPU.
*/ constructor(graphicsDevice, options){
super(), /**
* An array of index buffers. For unindexed meshes, this array can be empty. The first index
* buffer in the array is used by {@link MeshInstance}s with a `renderStyle` property set to
* {@link RENDERSTYLE_SOLID}. The second index buffer in the array is used if `renderStyle` is
* set to {@link RENDERSTYLE_WIREFRAME}.
*
* @type {IndexBuffer[]}
*/ this.indexBuffer = [
null
], /**
* The vertex buffer holding the vertex data of the mesh.
*
* @type {VertexBuffer}
*/ this.vertexBuffer = null, /**
* Array of primitive objects defining how vertex (and index) data in the mesh should be
* interpreted by the graphics device.
*
* - `type` is the type of primitive to render. Can be:
*
* - {@link PRIMITIVE_POINTS}
* - {@link PRIMITIVE_LINES}
* - {@link PRIMITIVE_LINELOOP}
* - {@link PRIMITIVE_LINESTRIP}
* - {@link PRIMITIVE_TRIANGLES}
* - {@link PRIMITIVE_TRISTRIP}
* - {@link PRIMITIVE_TRIFAN}
*
* - `base` is the offset of the first index or vertex to dispatch in the draw call.
* - `baseVertex` is the number added to each index value before indexing into the vertex buffers. (supported only in WebGPU, ignored in WebGL2)
* - `count` is the number of indices or vertices to dispatch in the draw call.
* - `indexed` specifies whether to interpret the primitive as indexed, thereby using the
* currently set index buffer.
*
* @type {{type: number, base: number, baseVertex: number, count: number, indexed?: boolean}[]}
*/ this.primitive = [
{
type: 0,
base: 0,
baseVertex: 0,
count: 0
}
], /**
* The skin data (if any) that drives skinned mesh animations for this mesh.
*
* @type {Skin|null}
*/ this.skin = null, /**
* Array of object space AABBs of vertices affected by each bone.
*
* @type {BoundingBox[]|null}
* @ignore
*/ this.boneAabb = null, /**
* Internal version of AABB, incremented when local AABB changes.
*
* @ignore
*/ this._aabbVer = 0, /**
* AABB representing object space bounds of the mesh.
*
* @type {BoundingBox}
* @private
*/ this._aabb = new BoundingBox(), /**
* @type {GeometryData|null}
* @private
*/ this._geometryData = null, /**
* @type {Morph|null}
* @private
*/ this._morph = null, /**
* True if the created index buffer should be accessible as a storage buffer in compute shader.
*
* @type {boolean}
* @private
*/ this._storageIndex = false, /**
* True if the created vertex buffer should be accessible as a storage buffer in compute shader.
*
* @type {boolean}
* @private
*/ this._storageVertex = false;
this.id = id++;
Debug.assert(graphicsDevice, 'Mesh constructor takes a GraphicsDevice as a parameter, and it was not provided.');
this.device = graphicsDevice;
this._storageIndex = options?.storageIndex || false;
this._storageVertex = options?.storageVertex || false;
}
/**
* Create a new Mesh instance from {@link Geometry} object.
*
* @param {GraphicsDevice} graphicsDevice - The graphics device used to manage this mesh.
* @param {Geometry} geometry - The geometry object to create the mesh from.
* @param {object} [options] - An object that specifies optional inputs for the function as follows:
* @param {boolean} [options.storageVertex] - Defines if the vertex buffer of the mesh can be used as
* a storage buffer by a compute shader. Defaults to false. Only supported on WebGPU.
* @param {boolean} [options.storageIndex] - Defines if the index buffer of the mesh can be used as
* a storage buffer by a compute shader. Defaults to false. Only supported on WebGPU.
* @returns {Mesh} A new mesh.
*/ static fromGeometry(graphicsDevice, geometry, options = {}) {
const mesh = new Mesh(graphicsDevice, options);
const { positions, normals, tangents, colors, uvs, uvs1, blendIndices, blendWeights, indices } = geometry;
if (positions) {
mesh.setPositions(positions);
}
if (normals) {
mesh.setNormals(normals);
}
if (tangents) {
mesh.setVertexStream(SEMANTIC_TANGENT, tangents, 4);
}
if (colors) {
mesh.setColors32(colors);
}
if (uvs) {
mesh.setUvs(0, uvs);
}
if (uvs1) {
mesh.setUvs(1, uvs1);
}
if (blendIndices) {
mesh.setVertexStream(SEMANTIC_BLENDINDICES, blendIndices, 4, blendIndices.length / 4, TYPE_UINT8);
}
if (blendWeights) {
mesh.setVertexStream(SEMANTIC_BLENDWEIGHT, blendWeights, 4);
}
if (indices) {
mesh.setIndices(indices);
}
mesh.update();
return mesh;
}
/**
* Sets the morph data that drives morph target animations for this mesh. Set to null if
* morphing is not used.
*
* @type {Morph|null}
*/ set morph(morph) {
if (morph !== this._morph) {
if (this._morph) {
this._morph.decRefCount();
}
this._morph = morph;
if (morph) {
morph.incRefCount();
}
}
}
/**
* Gets the morph data that drives morph target animations for this mesh.
*
* @type {Morph|null}
*/ get morph() {
return this._morph;
}
/**
* Sets the axis-aligned bounding box for the object space vertices of this mesh.
*
* @type {BoundingBox}
*/ set aabb(aabb) {
this._aabb = aabb;
this._aabbVer++;
}
/**
* Gets the axis-aligned bounding box for the object space vertices of this mesh.
*
* @type {BoundingBox}
*/ get aabb() {
return this._aabb;
}
/**
* Destroys the {@link VertexBuffer} and {@link IndexBuffer}s associated with the mesh. This is
* normally called by {@link Model#destroy} and does not need to be called manually.
*/ destroy() {
const morph = this.morph;
if (morph) {
// this decreases ref count on the morph
this.morph = null;
// destroy morph
if (morph.refCount < 1) {
morph.destroy();
}
}
if (this.vertexBuffer) {
this.vertexBuffer.destroy();
this.vertexBuffer = null;
}
for(let j = 0; j < this.indexBuffer.length; j++){
this._destroyIndexBuffer(j);
}
this.indexBuffer.length = 0;
this._geometryData = null;
}
_destroyIndexBuffer(index) {
if (this.indexBuffer[index]) {
this.indexBuffer[index].destroy();
this.indexBuffer[index] = null;
}
}
// initializes local bounding boxes for each bone based on vertices affected by the bone
// if morph targets are provided, it also adjusts local bone bounding boxes by maximum morph displacement
_initBoneAabbs(morphTargets) {
this.boneAabb = [];
this.boneUsed = [];
let x, y, z;
let bMax, bMin;
const boneMin = [];
const boneMax = [];
const boneUsed = this.boneUsed;
const numBones = this.skin.boneNames.length;
let maxMorphX, maxMorphY, maxMorphZ;
// start with empty bone bounds
for(let i = 0; i < numBones; i++){
boneMin[i] = new Vec3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
boneMax[i] = new Vec3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
}
// access to mesh from vertex buffer
const iterator = new VertexIterator(this.vertexBuffer);
const posElement = iterator.element[SEMANTIC_POSITION];
const weightsElement = iterator.element[SEMANTIC_BLENDWEIGHT];
const indicesElement = iterator.element[SEMANTIC_BLENDINDICES];
// Find bone AABBs of attached vertices
const numVerts = this.vertexBuffer.numVertices;
for(let j = 0; j < numVerts; j++){
for(let k = 0; k < 4; k++){
const boneWeight = weightsElement.array[weightsElement.index + k];
if (boneWeight > 0) {
const boneIndex = indicesElement.array[indicesElement.index + k];
boneUsed[boneIndex] = true;
x = posElement.array[posElement.index];
y = posElement.array[posElement.index + 1];
z = posElement.array[posElement.index + 2];
// adjust bounds of a bone by the vertex
bMax = boneMax[boneIndex];
bMin = boneMin[boneIndex];
if (bMin.x > x) bMin.x = x;
if (bMin.y > y) bMin.y = y;
if (bMin.z > z) bMin.z = z;
if (bMax.x < x) bMax.x = x;
if (bMax.y < y) bMax.y = y;
if (bMax.z < z) bMax.z = z;
if (morphTargets) {
// find maximum displacement of the vertex by all targets
let minMorphX = maxMorphX = x;
let minMorphY = maxMorphY = y;
let minMorphZ = maxMorphZ = z;
// morph this vertex by all morph targets
for(let l = 0; l < morphTargets.length; l++){
const target = morphTargets[l];
const dx = target.deltaPositions[j * 3];
const dy = target.deltaPositions[j * 3 + 1];
const dz = target.deltaPositions[j * 3 + 2];
if (dx < 0) {
minMorphX += dx;
} else {
maxMorphX += dx;
}
if (dy < 0) {
minMorphY += dy;
} else {
maxMorphY += dy;
}
if (dz < 0) {
minMorphZ += dz;
} else {
maxMorphZ += dz;
}
}
if (bMin.x > minMorphX) bMin.x = minMorphX;
if (bMin.y > minMorphY) bMin.y = minMorphY;
if (bMin.z > minMorphZ) bMin.z = minMorphZ;
if (bMax.x < maxMorphX) bMax.x = maxMorphX;
if (bMax.y < maxMorphY) bMax.y = maxMorphY;
if (bMax.z < maxMorphZ) bMax.z = maxMorphZ;
}
}
}
iterator.next();
}
// account for normalized positional data
const positionElement = this.vertexBuffer.getFormat().elements.find((e)=>e.name === SEMANTIC_POSITION);
if (positionElement && positionElement.normalize) {
const func = (()=>{
switch(positionElement.dataType){
case TYPE_INT8:
return (x)=>Math.max(x / 127.0, -1);
case TYPE_UINT8:
return (x)=>x / 255.0;
case TYPE_INT16:
return (x)=>Math.max(x / 32767.0, -1);
case TYPE_UINT16:
return (x)=>x / 65535.0;
default:
return (x)=>x;
}
})();
for(let i = 0; i < numBones; i++){
if (boneUsed[i]) {
const min = boneMin[i];
const max = boneMax[i];
min.set(func(min.x), func(min.y), func(min.z));
max.set(func(max.x), func(max.y), func(max.z));
}
}
}
// store bone bounding boxes
for(let i = 0; i < numBones; i++){
const aabb = new BoundingBox();
aabb.setMinMax(boneMin[i], boneMax[i]);
this.boneAabb.push(aabb);
}
}
// when mesh API to modify vertex / index data are used, this allocates structure to store the data
_initGeometryData() {
if (!this._geometryData) {
this._geometryData = new GeometryData();
// if vertex buffer exists already, store the sizes
if (this.vertexBuffer) {
this._geometryData.vertexCount = this.vertexBuffer.numVertices;
this._geometryData.maxVertices = this.vertexBuffer.numVertices;
}
// if index buffer exists already, store the sizes
if (this.indexBuffer.length > 0 && this.indexBuffer[0]) {
this._geometryData.indexCount = this.indexBuffer[0].numIndices;
this._geometryData.maxIndices = this.indexBuffer[0].numIndices;
}
}
}
/**
* Clears the mesh of existing vertices and indices and resets the {@link VertexFormat}
* associated with the mesh. This call is typically followed by calls to methods such as
* {@link Mesh#setPositions}, {@link Mesh#setVertexStream} or {@link Mesh#setIndices} and
* finally {@link Mesh#update} to rebuild the mesh, allowing different {@link VertexFormat}.
*
* @param {boolean} [verticesDynamic] - Indicates the {@link VertexBuffer} should be created
* with {@link BUFFER_DYNAMIC} usage. If not specified, {@link BUFFER_STATIC} is used.
* @param {boolean} [indicesDynamic] - Indicates the {@link IndexBuffer} should be created with
* {@link BUFFER_DYNAMIC} usage. If not specified, {@link BUFFER_STATIC} is used.
* @param {number} [maxVertices] - A {@link VertexBuffer} will be allocated with at least
* maxVertices, allowing additional vertices to be added to it without the allocation. If no
* value is provided, a size to fit the provided vertices will be allocated.
* @param {number} [maxIndices] - An {@link IndexBuffer} will be allocated with at least
* maxIndices, allowing additional indices to be added to it without the allocation. If no
* value is provided, a size to fit the provided indices will be allocated.
*/ clear(verticesDynamic, indicesDynamic, maxVertices = 0, maxIndices = 0) {
this._initGeometryData();
this._geometryData.initDefaults();
this._geometryData.recreate = true;
this._geometryData.maxVertices = maxVertices;
this._geometryData.maxIndices = maxIndices;
this._geometryData.verticesUsage = verticesDynamic ? BUFFER_STATIC : BUFFER_DYNAMIC;
this._geometryData.indicesUsage = indicesDynamic ? BUFFER_STATIC : BUFFER_DYNAMIC;
}
/**
* Sets the vertex data for any supported semantic.
*
* @param {string} semantic - The meaning of the vertex element. For supported semantics, see
* SEMANTIC_* in {@link VertexFormat}.
* @param {number[]|ArrayBufferView} data - Vertex data for the specified semantic.
* @param {number} componentCount - The number of values that form a single Vertex element. For
* example when setting a 3D position represented by 3 numbers per vertex, number 3 should be
* specified.
* @param {number} [numVertices] - The number of vertices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
* @param {number} [dataType] - The format of data when stored in the {@link VertexBuffer}, see
* TYPE_* in {@link VertexFormat}. When not specified, {@link TYPE_FLOAT32} is used.
* @param {boolean} [dataTypeNormalize] - If true, vertex attribute data will be mapped from a
* 0 to 255 range down to 0 to 1 when fed to a shader. If false, vertex attribute data is left
* unchanged. If this property is unspecified, false is assumed.
* @param {boolean} [asInt] - If true, vertex attribute data will be accessible as integer
* numbers in shader code. Defaults to false, which means that vertex attribute data will be
* accessible as floating point numbers. Can be only used with INT and UINT data types.
*/ setVertexStream(semantic, data, componentCount, numVertices, dataType = TYPE_FLOAT32, dataTypeNormalize = false, asInt = false) {
this._initGeometryData();
const vertexCount = numVertices || data.length / componentCount;
this._geometryData._changeVertexCount(vertexCount, semantic);
this._geometryData.vertexStreamsUpdated = true;
this._geometryData.vertexStreamDictionary[semantic] = new GeometryVertexStream(data, componentCount, dataType, dataTypeNormalize, asInt);
}
/**
* Gets the vertex data corresponding to a semantic.
*
* @param {string} semantic - The semantic of the vertex element to get. For supported
* semantics, see SEMANTIC_* in {@link VertexFormat}.
* @param {number[]|ArrayBufferView} data - An array to populate with the vertex data. When
* typed array is supplied, enough space needs to be reserved, otherwise only partial data is
* copied.
* @returns {number} Returns the number of vertices populated.
*/ getVertexStream(semantic, data) {
let count = 0;
let done = false;
// see if we have un-applied stream
if (this._geometryData) {
const stream = this._geometryData.vertexStreamDictionary[semantic];
if (stream) {
done = true;
count = this._geometryData.vertexCount;
if (ArrayBuffer.isView(data)) {
// destination data is typed array
data.set(stream.data);
} else {
// destination data is array
data.length = 0;
data.push(stream.data);
}
}
}
if (!done) {
// get stream from VertexBuffer
if (this.vertexBuffer) {
// note: there is no need to .end the iterator, as we are only reading data from it
const iterator = new VertexIterator(this.vertexBuffer);
count = iterator.readData(semantic, data);
}
}
return count;
}
/**
* Sets the vertex positions array. Vertices are stored using {@link TYPE_FLOAT32} format.
*
* @param {number[]|ArrayBufferView} positions - Vertex data containing positions.
* @param {number} [componentCount] - The number of values that form a single position element.
* Defaults to 3 if not specified, corresponding to x, y and z coordinates.
* @param {number} [numVertices] - The number of vertices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
*/ setPositions(positions, componentCount = GeometryData.DEFAULT_COMPONENTS_POSITION, numVertices) {
this.setVertexStream(SEMANTIC_POSITION, positions, componentCount, numVertices, TYPE_FLOAT32, false);
}
/**
* Sets the vertex normals array. Normals are stored using {@link TYPE_FLOAT32} format.
*
* @param {number[]|ArrayBufferView} normals - Vertex data containing normals.
* @param {number} [componentCount] - The number of values that form a single normal element.
* Defaults to 3 if not specified, corresponding to x, y and z direction.
* @param {number} [numVertices] - The number of vertices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
*/ setNormals(normals, componentCount = GeometryData.DEFAULT_COMPONENTS_NORMAL, numVertices) {
this.setVertexStream(SEMANTIC_NORMAL, normals, componentCount, numVertices, TYPE_FLOAT32, false);
}
/**
* Sets the vertex uv array. Uvs are stored using {@link TYPE_FLOAT32} format.
*
* @param {number} channel - The uv channel in [0..7] range.
* @param {number[]|ArrayBufferView} uvs - Vertex data containing uv-coordinates.
* @param {number} [componentCount] - The number of values that form a single uv element.
* Defaults to 2 if not specified, corresponding to u and v coordinates.
* @param {number} [numVertices] - The number of vertices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
*/ setUvs(channel, uvs, componentCount = GeometryData.DEFAULT_COMPONENTS_UV, numVertices) {
this.setVertexStream(SEMANTIC_TEXCOORD + channel, uvs, componentCount, numVertices, TYPE_FLOAT32, false);
}
/**
* Sets the vertex color array. Colors are stored using {@link TYPE_FLOAT32} format, which is
* useful for HDR colors.
*
* @param {number[]|ArrayBufferView} colors - Vertex data containing colors.
* @param {number} [componentCount] - The number of values that form a single color element.
* Defaults to 4 if not specified, corresponding to r, g, b and a.
* @param {number} [numVertices] - The number of vertices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
*/ setColors(colors, componentCount = GeometryData.DEFAULT_COMPONENTS_COLORS, numVertices) {
this.setVertexStream(SEMANTIC_COLOR, colors, componentCount, numVertices, TYPE_FLOAT32, false);
}
/**
* Sets the vertex color array. Colors are stored using {@link TYPE_UINT8} format, which is
* useful for LDR colors. Values in the array are expected in [0..255] range, and are mapped to
* [0..1] range in the shader.
*
* @param {number[]|ArrayBufferView} colors - Vertex data containing colors. The array is
* expected to contain 4 components per vertex, corresponding to r, g, b and a.
* @param {number} [numVertices] - The number of vertices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
*/ setColors32(colors, numVertices) {
this.setVertexStream(SEMANTIC_COLOR, colors, GeometryData.DEFAULT_COMPONENTS_COLORS, numVertices, TYPE_UINT8, true);
}
/**
* Sets the index array. Indices are stored using 16-bit format by default, unless more than
* 65535 vertices are specified, in which case 32-bit format is used.
*
* @param {number[]|Uint8Array|Uint16Array|Uint32Array} indices - The array of indices that
* define primitives (lines, triangles, etc.).
* @param {number} [numIndices] - The number of indices to be used from data array. If not
* provided, the whole data array is used. This allows to use only part of the data array.
*/ setIndices(indices, numIndices) {
this._initGeometryData();
this._geometryData.indexStreamUpdated = true;
this._geometryData.indices = indices;
this._geometryData.indexCount = numIndices || indices.length;
}
/**
* Gets the vertex positions data.
*
* @param {number[]|ArrayBufferView} positions - An array to populate with the vertex data.
* When typed array is supplied, enough space needs to be reserved, otherwise only partial data
* is copied.
* @returns {number} Returns the number of vertices populated.
*/ getPositions(positions) {
return this.getVertexStream(SEMANTIC_POSITION, positions);
}
/**
* Gets the vertex normals data.
*
* @param {number[]|ArrayBufferView} normals - An array to populate with the vertex data. When
* typed array is supplied, enough space needs to be reserved, otherwise only partial data is
* copied.
* @returns {number} Returns the number of vertices populated.
*/ getNormals(normals) {
return this.getVertexStream(SEMANTIC_NORMAL, normals);
}
/**
* Gets the vertex uv data.
*
* @param {number} channel - The uv channel in [0..7] range.
* @param {number[]|ArrayBufferView} uvs - An array to populate with the vertex data. When
* typed array is supplied, enough space needs to be reserved, otherwise only partial data is
* copied.
* @returns {number} Returns the number of vertices populated.
*/ getUvs(channel, uvs) {
return this.getVertexStream(SEMANTIC_TEXCOORD + channel, uvs);
}
/**
* Gets the vertex color data.
*
* @param {number[]|ArrayBufferView} colors - An array to populate with the vertex data. When
* typed array is supplied, enough space needs to be reserved, otherwise only partial data is
* copied.
* @returns {number} Returns the number of vertices populated.
*/ getColors(colors) {
return this.getVertexStream(SEMANTIC_COLOR, colors);
}
/**
* Gets the index data.
*
* @param {number[]|Uint8Array|Uint16Array|Uint32Array} indices - An array to populate with the
* index data. When a typed array is supplied, enough space needs to be reserved, otherwise
* only partial data is copied.
* @returns {number} Returns the number of indices populated.
*/ getIndices(indices) {
let count = 0;
// see if we have un-applied indices
if (this._geometryData && this._geometryData.indices) {
const streamIndices = this._geometryData.indices;
count = this._geometryData.indexCount;
if (ArrayBuffer.isView(indices)) {
// destination data is typed array
indices.set(streamIndices);
} else {
// destination data is array
indices.length = 0;
for(let i = 0, il = streamIndices.length; i < il; i++){
indices.push(streamIndices[i]);
}
}
} else {
// get data from IndexBuffer
if (this.indexBuffer.length > 0 && this.indexBuffer[0]) {
const indexBuffer = this.indexBuffer[0];
count = indexBuffer.readData(indices);
}
}
return count;
}
/**
* Applies any changes to vertex stream and indices to mesh. This allocates or reallocates
* {@link vertexBuffer} or {@link indexBuffer} to fit all provided vertices and indices, and
* fills them with data.
*
* @param {number} [primitiveType] - The type of primitive to render. Can be:
*
* - {@link PRIMITIVE_POINTS}
* - {@link PRIMITIVE_LINES}
* - {@link PRIMITIVE_LINELOOP}
* - {@link PRIMITIVE_LINESTRIP}
* - {@link PRIMITIVE_TRIANGLES}
* - {@link PRIMITIVE_TRISTRIP}
* - {@link PRIMITIVE_TRIFAN}
*
* Defaults to {@link PRIMITIVE_TRIANGLES} if not specified.
* @param {boolean} [updateBoundingBox] - True to update bounding box. Bounding box is updated
* only if positions were set since last time update was called, and `componentCount` for
* position was 3, otherwise bounding box is not updated. See {@link Mesh#setPositions}.
* Defaults to true if not specified. Set this to false to avoid update of the bounding box and
* use aabb property to set it instead.
*/ update(primitiveType = PRIMITIVE_TRIANGLES, updateBoundingBox = true) {
if (this._geometryData) {
// update bounding box if needed
if (updateBoundingBox) {
// find vec3 position stream
const stream = this._geometryData.vertexStreamDictionary[SEMANTIC_POSITION];
if (stream) {
if (stream.componentCount === 3) {
this._aabb.compute(stream.data, this._geometryData.vertexCount);
this._aabbVer++;
}
}
}
// destroy vertex buffer if recreate was requested or if vertices don't fit
let destroyVB = this._geometryData.recreate;
if (this._geometryData.vertexCount > this._geometryData.maxVertices) {
destroyVB = true;
this._geometryData.maxVertices = this._geometryData.vertexCount;
}
if (destroyVB) {
if (this.vertexBuffer) {
this.vertexBuffer.destroy();
this.vertexBuffer = null;
}
}
// destroy index buffer if recreate was requested or if indices don't fit
let destroyIB = this._geometryData.recreate;
if (this._geometryData.indexCount > this._geometryData.maxIndices) {
destroyIB = true;
this._geometryData.maxIndices = this._geometryData.indexCount;
}
if (destroyIB) {
if (this.indexBuffer.length > 0 && this.indexBuffer[0]) {
this.indexBuffer[0].destroy();
this.indexBuffer[0] = null;
}
}
// update vertices if needed
if (this._geometryData.vertexStreamsUpdated) {
this._updateVertexBuffer();
}
// update indices if needed
if (this._geometryData.indexStreamUpdated) {
this._updateIndexBuffer();
}
// set up primitive parameters
this.primitive[0].type = primitiveType;
if (this.indexBuffer.length > 0 && this.indexBuffer[0]) {
if (this._geometryData.indexStreamUpdated) {
this.primitive[0].count = this._geometryData.indexCount;
this.primitive[0].indexed = true;
}
} else {
if (this._geometryData.vertexStreamsUpdated) {
this.primitive[0].count = this._geometryData.vertexCount;
this.primitive[0].indexed = false;
}
}
// counts can be changed on next frame, so set them to 0
this._geometryData.vertexCount = 0;
this._geometryData.indexCount = 0;
this._geometryData.vertexStreamsUpdated = false;
this._geometryData.indexStreamUpdated = false;
this._geometryData.recreate = false;
// update other render states
this.updateRenderStates();
}
}
// builds vertex format based on attached vertex streams
_buildVertexFormat(vertexCount) {
const vertexDesc = [];
for(const semantic in this._geometryData.vertexStreamDictionary){
const stream = this._geometryData.vertexStreamDictionary[semantic];
vertexDesc.push({
semantic: semantic,
components: stream.componentCount,
type: stream.dataType,
normalize: stream.dataTypeNormalize,
asInt: stream.asInt
});
}
return new VertexFormat(this.device, vertexDesc, vertexCount);
}
// copy attached data into vertex buffer
_updateVertexBuffer() {
// if we don't have vertex buffer, create new one, otherwise update existing one
if (!this.vertexBuffer) {
const allocateVertexCount = this._geometryData.maxVertices;
const format = this._buildVertexFormat(allocateVertexCount);
this.vertexBuffer = new VertexBuffer(this.device, format, allocateVertexCount, {
usage: this._geometryData.verticesUsage,
storage: this._storageVertex
});
}
// lock vertex buffer and create typed access arrays for individual elements
const iterator = new VertexIterator(this.vertexBuffer);
// copy all stream data into vertex buffer
const numVertices = this._geometryData.vertexCount;
for(const semantic in this._geometryData.vertexStreamDictionary){
const stream = this._geometryData.vertexStreamDictionary[semantic];
iterator.writeData(semantic, stream.data, numVertices);
// remove stream
delete this._geometryData.vertexStreamDictionary[semantic];
}
iterator.end();
}
// copy attached data into index buffer
_updateIndexBuffer() {
// if we don't have index buffer, create new one, otherwise update existing one
if (this.indexBuffer.length <= 0 || !this.indexBuffer[0]) {
const maxVertices = this._geometryData.maxVertices;
const createFormat = maxVertices > 0xffff || maxVertices === 0 ? INDEXFORMAT_UINT32 : INDEXFORMAT_UINT16;
const options = this._storageIndex ? {
storage: true
} : undefined;
this.indexBuffer[0] = new IndexBuffer(this.device, createFormat, this._geometryData.maxIndices, this._geometryData.indicesUsage, undefined, options);
}
const srcIndices = this._geometryData.indices;
if (srcIndices) {
const indexBuffer = this.indexBuffer[0];
indexBuffer.writeData(srcIndices, this._geometryData.indexCount);
// remove data
this._geometryData.indices = null;
}
}
// prepares the mesh to be rendered with specific render style
prepareRenderState(renderStyle) {
if (renderStyle === RENDERSTYLE_WIREFRAME) {
this.generateWireframe();
} else if (renderStyle === RENDERSTYLE_POINTS) {
this.primitive[RENDERSTYLE_POINTS] = {
type: PRIMITIVE_POINTS,
base: 0,
baseVertex: 0,
count: this.vertexBuffer ? this.vertexBuffer.numVertices : 0,
indexed: false
};
}
}
// updates existing render states with changes to solid render state
updateRenderStates() {
if (this.primitive[RENDERSTYLE_POINTS]) {
this.prepareRenderState(RENDERSTYLE_POINTS);
}
if (this.primitive[RENDERSTYLE_WIREFRAME]) {
this.prepareRenderState(RENDERSTYLE_WIREFRAME);
}
}
generateWireframe() {
// release existing IB
this._destroyIndexBuffer(RENDERSTYLE_WIREFRAME);
const numVertices = this.vertexBuffer.numVertices;
const lines = [];
let format;
if (this.indexBuffer.length > 0 && this.indexBuffer[0]) {
const offsets = [
[
0,
1
],
[
1,
2
],
[
2,
0
]
];
const base = this.primitive[RENDERSTYLE_SOLID].base;
const count = this.primitive[RENDERSTYLE_SOLID].count;
const baseVertex = this.primitive[RENDERSTYLE_SOLID].baseVertex || 0;
const indexBuffer = this.indexBuffer[RENDERSTYLE_SOLID];
const srcIndices = new typedArrayIndexFormats[indexBuffer.format](indexBuffer.storage);
const seen = new Set();
for(let j = base; j < base + count; j += 3){
for(let k = 0; k < 3; k++){
const i1 = srcIndices[j + offsets[k][0]] + baseVertex;
const i2 = srcIndices[j + offsets[k][1]] + baseVertex;
const hash = i1 > i2 ? i2 * numVertices + i1 : i1 * numVertices + i2;
if (!seen.has(hash)) {
seen.add(hash);
lines.push(i1, i2);
}
}
}
format = indexBuffer.format;
} else {
for(let i = 0; i < numVertices; i += 3){
lines.push(i, i + 1, i + 1, i + 2, i + 2, i);
}
format = lines.length > 65535 ? INDEXFORMAT_UINT32 : INDEXFORMAT_UINT16;
}
const wireBuffer = new IndexBuffer(this.vertexBuffer.device, format, lines.length);
const dstIndices = new typedArrayIndexFormats[wireBuffer.format](wireBuffer.storage);
dstIndices.set(lines);
wireBuffer.unlock();
this.primitive[RENDERSTYLE_WIREFRAME] = {
type: PRIMITIVE_LINES,
base: 0,
baseVertex: 0,
count: lines.length,
indexed: true
};
this.indexBuffer[RENDERSTYLE_WIREFRAME] = wireBuffer;
}
}
export { Mesh };