nanogl-gltf
Version:
284 lines (283 loc) • 10.3 kB
JavaScript
import GLArrayBuffer from 'nanogl/arraybuffer';
import Vao from 'nanogl-vao';
import GLIndexBuffer from 'nanogl/indexbuffer';
import Gltf2 from '../types/Gltf2';
import GltfTypes from '../types/GltfTypes';
import Gltf from '../Gltf';
import Bounds from 'nanogl-pbr/Bounds';
/**
* A helper class to manage a Primitive's attribute (linking an attribute name to its Accessor)
*/
export class Attribute {
/**
* @param semantic Attribute's semantic
* @param accessor Attribute's Accessor
*/
constructor(semantic, accessor) {
this.semantic = semantic;
this.accessor = accessor;
this.glslname = Gltf.getSemantics().getAttributeName(semantic);
}
}
/**
* A helper class to manage a set of Attributes sharing the same BufferView
*/
export class BufferInfos {
constructor(accessor) {
this.accessor = accessor;
this.attributes = [];
}
addAttribute(attribute) {
this.attributes.push(attribute);
}
}
/**
* A helper class to manage an array of Attributes
*/
export class AttributesSet {
constructor() {
this._attributes = [];
}
get length() {
return this._attributes.length;
}
get attributes() {
return this._attributes;
}
/**
* Add an attribute to the set
* @param attribute Attribute to add
*/
add(attribute) {
this._attributes.push(attribute);
}
/**
* Get an Attribute by its semantic (POSITION, NORMAL, TEXCOORD_0, ...)
* @param semantic Semantic of the attribute to get
*/
getSemantic(semantic) {
for (const a of this._attributes) {
if (a.semantic === semantic)
return a;
}
return null;
}
/*
* Get sets of attributes ordered by BufferView
*/
getBuffersViewSets() {
const map = new Map();
for (const a of this._attributes) {
const bId = a.accessor.bufferView;
if (!map.has(bId)) {
map.set(bId, new BufferInfos(a.accessor));
}
map.get(bId).addAttribute(a);
}
return Array.from(map.values());
}
}
const ELEMENT_ARRAY_BUFFER = 0x8893;
const ARRAY_BUFFER = 0x8892;
/**
* The Primitive element is a set of attributes defining a geometry to render with a given material and in a given mode.
* It may be indexed, and may have morph targets.
*
* It's the most basic renderable element, as a Scene is made of Meshes, and a Mesh is made of Primitives.
*/
export default class Primitive {
constructor() {
this.gltftype = GltfTypes.PRIMITIVE;
/**
* The Material to use for this Primitive.
* If not defined, a default material with no effect will be used.
*/
this.material = null;
/**
* The indices to use to render this Primitive, if it is indexed.
* If not indexed, the Primitive is rendered in vertices' order.
*/
this.indices = null;
/**
* The Morph Targets to use to render this Primitive, if it is morphed.
*/
this.targets = null;
/**
* The bounding box of this Primitive
*/
this.bounds = new Bounds();
}
/**
* Calculate the bounding box of this Primitive, based on its POSITION attribute.
*/
_calculaterBounds() {
const pos = this.attributes.getSemantic('POSITION');
if (pos != null && pos.accessor.min && pos.accessor.max) {
this.bounds.fromMinMax(pos.accessor.min, pos.accessor.max);
}
}
/**
* Parse the Primitive data, load the attributes, indices, material and morph targets, and calculate the bounding box.
*
* Is async as it needs to wait for the Accessors, and possible Material, to be created.
* @param gltfLoader GLTFLoader to use
* @param data Data to parse
*/
async parse(gltfLoader, data) {
this.attributes = new AttributesSet();
await this.parseAttributeSet(gltfLoader, this.attributes, data.attributes);
if (data.indices !== undefined)
this.indices = await gltfLoader.getElement(GltfTypes.ACCESSOR, data.indices);
if (data.material !== undefined) {
this.material = await gltfLoader.getElement(GltfTypes.MATERIAL, data.material);
}
else {
this.material = await gltfLoader.loadDefaultMaterial();
}
if (data.mode !== undefined)
this.mode = data.mode;
else
this.mode = Gltf2.MeshPrimitiveMode.DEFAULT;
if (data.targets !== undefined) {
this.targets = [];
for (let i = 0; i < data.targets.length; i++) {
const tgt = data.targets[i];
const aset = new AttributesSet();
await this.parseMorphAttributeSet(gltfLoader, aset, tgt, i);
this.targets.push(aset);
}
}
this._calculaterBounds();
}
/**
* Parse the attributes of a Primitive and store it in an AttributesSet.
*
* Is async as it needs to wait for the Attribute's Accessors to be loaded, or created if not yet loaded.
* @param gltfLoader GLTFLoader to use to load the Accessors
* @param aset AttributesSet to fill
* @param data Data to parse
*/
async parseAttributeSet(gltfLoader, aset, data) {
for (const attrib in data) {
const accessor = await gltfLoader.getElement(GltfTypes.ACCESSOR, data[attrib]);
aset.add(new Attribute(attrib, accessor));
}
}
/**
* Parse the attributes of a Primitive's Morph Target and store it in an AttributesSet.
*
* Is async as it needs to wait for the Attribute's Accessors to be loaded, or created if not yet loaded.
* @param gltfLoader GLTFLoader to use to load the Accessors
* @param aset AttributesSet to fill
* @param data Data to parse
* @param morphIndex Index of the Morph Target
*/
async parseMorphAttributeSet(gltfLoader, aset, data, morphIndex) {
for (const attrib in data) {
const accessor = await gltfLoader.getElement(GltfTypes.ACCESSOR, data[attrib]);
const attribute = new Attribute(attrib, accessor);
attribute.glslname = Gltf.getSemantics().getMorphedAttributeName(attribute.semantic, morphIndex);
aset.add(attribute);
}
}
/**
* Create the GLArrayBuffers for Primitive's attributes (basic & morph target) and GLIndexBuffer for Primitive's indices.
* After that, the Primitive is ready to be rendered.
* @param gl GL context to use
*/
allocateGl(gl) {
this._vaoMap = new Map();
this.buffers = [];
const buffersSet = this.attributes.getBuffersViewSets();
for (const set of buffersSet) {
this.buffers.push(this.createArrayBuffer(gl, set));
}
if (this.indices !== null) {
const glBuffer = this.indices.bufferView.getWebGLBuffer(gl, ELEMENT_ARRAY_BUFFER);
this.indexBuffer = new GLIndexBuffer(gl, this.indices.componentType, undefined, gl.STATIC_DRAW, glBuffer);
this.indexBuffer.byteLength = this.indices.bufferView.byteLength;
}
if (this.targets !== null) {
for (let i = 0; i < this.targets.length; i++) {
const target = this.targets[i];
const buffersSet = target.getBuffersViewSets();
for (const set of buffersSet) {
this.buffers.push(this.createArrayBuffer(gl, set));
}
}
}
}
/**
* Create a GLArrayBuffer from an attribute (or a set of attributes sharing the same BufferView)
* @param gl GL context to use
* @param set Set of attributes to create the array buffer from
*/
createArrayBuffer(gl, set) {
const bufferView = set.accessor.bufferView;
const glBuffer = bufferView.getWebGLBuffer(gl, ARRAY_BUFFER);
const glArraybuffer = new GLArrayBuffer(gl, undefined, gl.STATIC_DRAW, glBuffer);
glArraybuffer.byteLength = bufferView.byteLength;
glArraybuffer.stride = set.accessor._stride;
glArraybuffer._computeLength();
for (const attribute of set.attributes) {
const def = this.createAttributeDefinition(attribute);
glArraybuffer.attribs.push(def);
}
return glArraybuffer;
}
/**
* Create an attribute definition from an Attribute, to be used in a GLArrayBuffer
* @param attribute Attribute to create the definition from
*/
createAttributeDefinition(attribute) {
const accessor = attribute.accessor;
return {
name: attribute.glslname,
type: accessor.componentType,
size: accessor.numComps,
normalize: accessor.normalized,
offset: accessor.byteOffset,
stride: accessor._stride
};
}
/**
* Get the Primitive's VAO linked to a specific Program. If it doesn't exist yet, create it.
* @param prg
*/
getVao(prg) {
const id = prg._cuid.toString();
if (!this._vaoMap.has(id)) {
const vao = new Vao(prg.gl);
vao.setup(prg, this.buffers, this.indexBuffer);
this._vaoMap.set(id, vao);
}
return this._vaoMap.get(id);
}
/**
* Bind the VAO containing the Primitive's attributes and indices, ready to be drawn.
* Called by the MeshRenderer just before rendering the Primitive.
* @param prg
*/
bindVao(prg) {
this._currentVao = this.getVao(prg);
this._currentVao.bind();
}
/**
* Draw the Primitive, using its indices if it is indexed, or its vertices' order if it is not.
* Called by the MeshRenderer.
*/
render() {
if (this.indexBuffer) {
this.indexBuffer.draw(this.mode, this.indices.count, this.indices.byteOffset);
}
else
this.buffers[0].draw(this.mode);
}
/**
* Unbind the Primitive's VAO.
* Called by the MeshRenderer just after rendering the Primitive.
*/
unbindVao() {
this._currentVao.unbind();
}
}