export enum TYPES { "BYTE" = "BYTE", "UNSIGNED_BYTE" = "UNSIGNED_BYTE", "SHORT" = "SHORT", "UNSIGNED_SHORT" = "UNSIGNED_SHORT", "FLOAT" = "FLOAT", } export interface AttributeInfo { attribute: Attribute; size: Attribute["size"]; type: Attribute["type"]; normalized: Attribute["normalized"]; offset: number; stride: number; } /** * An exception for when two or more of the same attributes are found in the * same layout. * @private */ export class DuplicateAttributeException extends Error { /** * Create a DuplicateAttributeException * @param {Attribute} attribute - The attribute that was found more than * once in the {@link Layout} */ constructor(attribute: Attribute) { super(`found duplicate attribute: ${attribute.key}`); } } /** * Represents how a vertex attribute should be packed into an buffer. * @private */ export class Attribute { public sizeOfType: number; public sizeInBytes: number; /** * Create an attribute. Do not call this directly, use the predefined * constants. * @param {string} key - The name of this attribute as if it were a key in * an Object. Use the camel case version of the upper snake case * const name. * @param {number} size - The number of components per vertex attribute. * Must be 1, 2, 3, or 4. * @param {string} type - The data type of each component for this * attribute. Possible values:
* "BYTE": signed 8-bit integer, with values in [-128, 127]
* "SHORT": signed 16-bit integer, with values in * [-32768, 32767]
* "UNSIGNED_BYTE": unsigned 8-bit integer, with values in * [0, 255]
* "UNSIGNED_SHORT": unsigned 16-bit integer, with values in * [0, 65535]
* "FLOAT": 32-bit floating point number * @param {boolean} normalized - Whether integer data values should be * normalized when being casted to a float.
* If true, signed integers are normalized to [-1, 1].
* If true, unsigned integers are normalized to [0, 1].
* For type "FLOAT", this parameter has no effect. */ constructor(public key: string, public size: number, public type: TYPES, public normalized: boolean = false) { switch (type) { case "BYTE": case "UNSIGNED_BYTE": this.sizeOfType = 1; break; case "SHORT": case "UNSIGNED_SHORT": this.sizeOfType = 2; break; case "FLOAT": this.sizeOfType = 4; break; default: throw new Error(`Unknown gl type: ${type}`); } this.sizeInBytes = this.sizeOfType * size; } } /** * A class to represent the memory layout for a vertex attribute array. Used by * {@link Mesh}'s TBD(...) method to generate a packed array from mesh data. *

* Layout can sort of be thought of as a C-style struct declaration. * {@link Mesh}'s TBD(...) method will use the {@link Layout} instance to * pack an array in the given attribute order. *

* Layout also is very helpful when calling a WebGL context's * vertexAttribPointer method. If you've created a buffer using * a Layout instance, then the same Layout instance can be used to determine * the size, type, normalized, stride, and offset parameters for * vertexAttribPointer. *

* For example: *


 *
 * const index = glctx.getAttribLocation(shaderProgram, "pos");
 * glctx.vertexAttribPointer(
 *   layout.position.size,
 *   glctx[layout.position.type],
 *   layout.position.normalized,
 *   layout.position.stride,
 *   layout.position.offset);
 * 
* @see {@link Mesh} */ export class Layout { // Geometry attributes /** * Attribute layout to pack a vertex's x, y, & z as floats * * @see {@link Layout} */ static POSITION = new Attribute("position", 3, TYPES.FLOAT); /** * Attribute layout to pack a vertex's normal's x, y, & z as floats * * @see {@link Layout} */ static NORMAL = new Attribute("normal", 3, TYPES.FLOAT); /** * Attribute layout to pack a vertex's normal's x, y, & z as floats. *

* This value will be computed on-the-fly based on the texture coordinates. * If no texture coordinates are available, the generated value will default to * 0, 0, 0. * * @see {@link Layout} */ static TANGENT = new Attribute("tangent", 3, TYPES.FLOAT); /** * Attribute layout to pack a vertex's normal's bitangent x, y, & z as floats. *

* This value will be computed on-the-fly based on the texture coordinates. * If no texture coordinates are available, the generated value will default to * 0, 0, 0. * @see {@link Layout} */ static BITANGENT = new Attribute("bitangent", 3, TYPES.FLOAT); /** * Attribute layout to pack a vertex's texture coordinates' u & v as floats * * @see {@link Layout} */ static UV = new Attribute("uv", 2, TYPES.FLOAT); // Material attributes /** * Attribute layout to pack an unsigned short to be interpreted as a the index * into a {@link Mesh}'s materials list. *

* The intention of this value is to send all of the {@link Mesh}'s materials * into multiple shader uniforms and then reference the current one by this * vertex attribute. *

* example glsl code: * *


     *  // this is bound using MATERIAL_INDEX
     *  attribute int materialIndex;
     *
     *  struct Material {
     *    vec3 diffuse;
     *    vec3 specular;
     *    vec3 specularExponent;
     *  };
     *
     *  uniform Material materials[MAX_MATERIALS];
     *
     *  // ...
     *
     *  vec3 diffuse = materials[materialIndex];
     *
     * 
* TODO: More description & test to make sure subscripting by attributes even * works for webgl * * @see {@link Layout} */ static MATERIAL_INDEX = new Attribute("materialIndex", 1, TYPES.SHORT); static MATERIAL_ENABLED = new Attribute("materialEnabled", 1, TYPES.UNSIGNED_SHORT); static AMBIENT = new Attribute("ambient", 3, TYPES.FLOAT); static DIFFUSE = new Attribute("diffuse", 3, TYPES.FLOAT); static SPECULAR = new Attribute("specular", 3, TYPES.FLOAT); static SPECULAR_EXPONENT = new Attribute("specularExponent", 3, TYPES.FLOAT); static EMISSIVE = new Attribute("emissive", 3, TYPES.FLOAT); static TRANSMISSION_FILTER = new Attribute("transmissionFilter", 3, TYPES.FLOAT); static DISSOLVE = new Attribute("dissolve", 1, TYPES.FLOAT); static ILLUMINATION = new Attribute("illumination", 1, TYPES.UNSIGNED_SHORT); static REFRACTION_INDEX = new Attribute("refractionIndex", 1, TYPES.FLOAT); static SHARPNESS = new Attribute("sharpness", 1, TYPES.FLOAT); static MAP_DIFFUSE = new Attribute("mapDiffuse", 1, TYPES.SHORT); static MAP_AMBIENT = new Attribute("mapAmbient", 1, TYPES.SHORT); static MAP_SPECULAR = new Attribute("mapSpecular", 1, TYPES.SHORT); static MAP_SPECULAR_EXPONENT = new Attribute("mapSpecularExponent", 1, TYPES.SHORT); static MAP_DISSOLVE = new Attribute("mapDissolve", 1, TYPES.SHORT); static ANTI_ALIASING = new Attribute("antiAliasing", 1, TYPES.UNSIGNED_SHORT); static MAP_BUMP = new Attribute("mapBump", 1, TYPES.SHORT); static MAP_DISPLACEMENT = new Attribute("mapDisplacement", 1, TYPES.SHORT); static MAP_DECAL = new Attribute("mapDecal", 1, TYPES.SHORT); static MAP_EMISSIVE = new Attribute("mapEmissive", 1, TYPES.SHORT); public stride: number; public attributes: Attribute[]; public attributeMap: { [idx: string]: AttributeInfo }; /** * Create a Layout object. This constructor will throw if any duplicate * attributes are given. * @param {Array} ...attributes - An ordered list of attributes that * describe the desired memory layout for each vertex attribute. *

* * @see {@link Mesh} */ constructor(...attributes: Attribute[]) { this.attributes = attributes; this.attributeMap = {}; let offset = 0; let maxStrideMultiple = 0; for (const attribute of attributes) { if (this.attributeMap[attribute.key]) { throw new DuplicateAttributeException(attribute); } // Add padding to satisfy WebGL's requirement that all // vertexAttribPointer calls have an offset that is a multiple of // the type size. if (offset % attribute.sizeOfType !== 0) { offset += attribute.sizeOfType - (offset % attribute.sizeOfType); console.warn("Layout requires padding before " + attribute.key + " attribute"); } this.attributeMap[attribute.key] = { attribute: attribute, size: attribute.size, type: attribute.type, normalized: attribute.normalized, offset: offset, } as AttributeInfo; offset += attribute.sizeInBytes; maxStrideMultiple = Math.max(maxStrideMultiple, attribute.sizeOfType); } // Add padding to the end to satisfy WebGL's requirement that all // vertexAttribPointer calls have a stride that is a multiple of the // type size. Because we're putting differently sized attributes into // the same buffer, it must be padded to a multiple of the largest // type size. if (offset % maxStrideMultiple !== 0) { offset += maxStrideMultiple - (offset % maxStrideMultiple); console.warn("Layout requires padding at the back"); } this.stride = offset; for (const attribute of attributes) { this.attributeMap[attribute.key].stride = this.stride; } } }