@itwin/core-frontend
Version:
iTwin.js frontend components
288 lines • 11.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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