UNPKG

@itwin/core-frontend

Version:
288 lines • 11.6 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Rendering */ import { assert } from "@itwin/core-bentley"; import { AuxChannelDataType, Point3d, Range1d, Range3d, Vector3d, } from "@itwin/core-geometry"; import { OctEncodedNormal, QParams3d, QPoint3d, Quantization } from "@itwin/core-common"; import { computeDimensions } from "./VertexTable"; /** @alpha */ export class AuxChannel { name; inputs; indices; constructor(props) { this.name = props.name; this.inputs = props.inputs; this.indices = props.indices; } toJSON() { return { name: this.name, inputs: this.inputs, indices: this.indices, }; } } /** @alpha */ export class AuxDisplacementChannel extends AuxChannel { qOrigin; qScale; constructor(props) { super(props); this.qOrigin = Float32Array.from(props.qOrigin); this.qScale = Float32Array.from(props.qScale); } toJSON() { return { ...super.toJSON(), qOrigin: Array.from(this.qOrigin), qScale: Array.from(this.qScale), }; } } /** @alpha */ export class AuxParamChannel extends AuxChannel { qOrigin; qScale; constructor(props) { super(props); this.qOrigin = props.qOrigin[0]; this.qScale = props.qScale[0]; } toJSON() { return { ...super.toJSON(), qOrigin: [this.qOrigin], qScale: [this.qScale], }; } } /** * Represents one or more channels of auxiliary per-vertex data which can be used to animate and resymbolize a mesh in various ways. * Each channel holds a fixed number of bytes for each vertex (typically 2 bytes for normals and params, 6 bytes for displacements). * The channels are interleaved in a rectangular array such that the data for each vertex is stored contiguously; that is, if a displacement and * a normal channel exist, then the first vertex's displacement is followed by the first vertex's normal, which is followed by the second * vertex's displacement and normal; and so on. * @alpha */ export class AuxChannelTable { /** Rectangular array of per-vertex data, of size width * height * numBytesPerVertex bytes. */ data; /** The number of 4-byte RGBA columns in each row of the array. */ width; /** The number of rows in the array. */ height; /** The number of vertices in the array. Must be no more than (width * height) / numBytesPerVertex. */ numVertices; /** The number of bytes allocated for each vertex. Must be a multiple of two. */ numBytesPerVertex; /** Displacements used for animations. */ displacements; /** Normals used for animations. */ normals; /** Scalar params used for animations. */ params; constructor(props, displacements, normals, params) { this.data = props.data; this.width = props.width; this.height = props.height; this.numVertices = props.count; this.numBytesPerVertex = props.numBytesPerVertex; this.displacements = displacements; this.normals = normals; this.params = params; } static fromJSON(props) { let displacements; let normals; let params; if (undefined !== props.displacements && 0 < props.displacements.length) { displacements = []; for (const displacement of props.displacements) displacements.push(new AuxDisplacementChannel(displacement)); } if (undefined !== props.normals && 0 < props.normals.length) { normals = []; for (const normal of props.normals) normals.push(new AuxChannel(normal)); } if (undefined !== props.params && 0 < props.params.length) { params = []; for (const param of props.params) params.push(new AuxParamChannel(param)); } return undefined !== displacements || undefined !== normals || undefined !== params ? new AuxChannelTable(props, displacements, normals, params) : undefined; } toJSON() { return { data: this.data, width: this.width, height: this.height, count: this.numVertices, numBytesPerVertex: this.numBytesPerVertex, displacements: this.displacements?.map((x) => x.toJSON()), normals: this.normals?.map((x) => x.toJSON()), params: this.params?.map((x) => x.toJSON()), }; } static fromChannels(channels, numVertices, maxDimension) { return AuxChannelTableBuilder.buildAuxChannelTable(channels, numVertices, maxDimension); } } function invert(num) { if (0 !== num) num = 1 / num; return num; } class AuxChannelTableBuilder { _view; _props; _numBytesPerVertex; constructor(props, numBytesPerVertex) { this._props = props; this._numBytesPerVertex = numBytesPerVertex; this._view = new DataView(props.data.buffer); } static buildAuxChannelTable(channels, numVertices, maxDimension) { const numBytesPerVertex = channels.reduce((accum, channel) => accum + computeNumBytesPerVertex(channel), 0); if (!numBytesPerVertex) return undefined; const nRgbaPerVertex = Math.floor((numBytesPerVertex + 3) / 4); const nUnusedBytesPerVertex = nRgbaPerVertex * 4 - numBytesPerVertex; assert(0 === nUnusedBytesPerVertex || 2 === nUnusedBytesPerVertex); // We don't want any unused bytes. If we've got 2 extra, make every other vertex's channel start in the middle of the first texel. let dimensions; if (0 !== nUnusedBytesPerVertex) dimensions = computeDimensions(Math.floor((numVertices + 1) / 2), numBytesPerVertex / 2, 0, maxDimension); // twice as many RGBA for half as many vertices. else dimensions = computeDimensions(numVertices, nRgbaPerVertex, 0, maxDimension); const data = new Uint8Array(dimensions.width * dimensions.height * 4); const props = { data, width: dimensions.width, height: dimensions.height, count: numVertices, numBytesPerVertex, }; const builder = new AuxChannelTableBuilder(props, numBytesPerVertex); builder.build(channels); return AuxChannelTable.fromJSON(props); } build(channels) { let byteOffset = 0; for (const channel of channels) { if (AuxChannelDataType.Normal === channel.dataType) this.addNormals(channel, byteOffset); else if (AuxChannelDataType.Vector === channel.dataType) this.addDisplacements(channel, byteOffset); else this.addParams(channel, byteOffset); byteOffset += computeNumBytesPerVertex(channel); } } addNormals(channel, byteOffset) { const inputs = []; const indices = []; const normal = new Vector3d(); for (let i = 0; i < channel.data.length; i++) { let byteIndex = byteOffset + i * 2; // 2 bytes per normal indices.push(byteIndex / 2); // indices aligned to 2-byte intervals const data = channel.data[i]; inputs.push(data.input); for (let j = 0; j < data.values.length; j += 3) { normal.x = data.values[j]; normal.y = data.values[j + 1]; normal.z = data.values[j + 2]; normal.normalizeInPlace(); const encodedNormal = OctEncodedNormal.encode(normal); this._view.setUint16(byteIndex, encodedNormal, true); byteIndex += this._numBytesPerVertex; } } const normals = this._props.normals ?? (this._props.normals = []); normals.push({ name: channel.name ?? "", inputs, indices, }); } addParams(channel, byteOffset) { const inputs = []; const indices = []; const range = Range1d.createNull(); for (const data of channel.data) { inputs.push(data.input); range.extendArray(data.values); } const qScale = Quantization.computeScale(range.high - range.low); for (let i = 0; i < channel.data.length; i++) { let byteIndex = byteOffset + i * 2; // 2 bytes per double indices.push(byteIndex / 2); // indices aligned to 2-byte intervals for (const value of channel.data[i].values) { const quantized = Quantization.quantize(value, range.low, qScale); this._view.setUint16(byteIndex, quantized, true); byteIndex += this._numBytesPerVertex; } } const params = this._props.params ?? (this._props.params = []); params.push({ inputs, indices, name: channel.name ?? "", qOrigin: [range.low], qScale: [invert(qScale)], }); } addDisplacements(channel, byteOffset) { const inputs = []; const indices = []; const point = new Point3d(); const range = Range3d.createNull(); for (const data of channel.data) { inputs.push(data.input); for (let i = 0; i < data.values.length; i += 3) { point.set(data.values[i], data.values[i + 1], data.values[i + 2]); range.extend(point); } } const qParams = QParams3d.fromRange(range); const qPoint = new QPoint3d(); for (let i = 0; i < channel.data.length; i++) { let byteIndex = byteOffset + i * 6; // 2 bytes per coordinate indices.push(byteIndex / 2); // indices aligned to 2-byte intervals const data = channel.data[i]; for (let j = 0; j < data.values.length; j += 3) { point.set(data.values[j], data.values[j + 1], data.values[j + 2]); qPoint.init(point, qParams); this._view.setUint16(byteIndex + 0, qPoint.x, true); this._view.setUint16(byteIndex + 2, qPoint.y, true); this._view.setUint16(byteIndex + 4, qPoint.z, true); byteIndex += this._numBytesPerVertex; } } const displacements = this._props.displacements ?? (this._props.displacements = []); displacements.push({ inputs, indices, name: channel.name ?? "", qOrigin: qParams.origin.toArray(), qScale: qParams.scale.toArray().map((x) => invert(x)), }); } } function computeNumBytesPerVertex(channel) { const nEntries = channel.data.length; switch (channel.dataType) { case AuxChannelDataType.Vector: return 6 * nEntries; // 3 16-bit quantized coordinate values per entry. case AuxChannelDataType.Normal: case AuxChannelDataType.Distance: case AuxChannelDataType.Scalar: return 2 * nEntries; // 1 16-bit quantized value per entry. } } //# sourceMappingURL=AuxChannelTable.js.map