@deck.gl/mesh-layers
Version:
deck.gl layers that loads 3D meshes or scene graphs
230 lines • 8.83 kB
JavaScript
// Note: This file will either be moved back to deck.gl or reformatted to web-monorepo standards
// Disabling lint temporarily to facilitate copying code in and out of this repo
/* eslint-disable */
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { Layer, project32, picking, log } from '@deck.gl/core';
import { Texture } from '@luma.gl/core';
import { Model, Geometry } from '@luma.gl/engine';
import { phongMaterial } from '@luma.gl/shadertools';
import { MATRIX_ATTRIBUTES, shouldComposeModelMatrix } from "../utils/matrix.js";
import { simpleMeshUniforms } from "./simple-mesh-layer-uniforms.js";
import vs from "./simple-mesh-layer-vertex.glsl.js";
import fs from "./simple-mesh-layer-fragment.glsl.js";
import { getMeshBoundingBox } from '@loaders.gl/schema';
function normalizeGeometryAttributes(attributes) {
const positionAttribute = attributes.positions || attributes.POSITION;
log.assert(positionAttribute, 'no "postions" or "POSITION" attribute in mesh');
const vertexCount = positionAttribute.value.length / positionAttribute.size;
let colorAttribute = attributes.COLOR_0 || attributes.colors;
if (!colorAttribute) {
colorAttribute = { size: 3, value: new Float32Array(vertexCount * 3).fill(1) };
}
let normalAttribute = attributes.NORMAL || attributes.normals;
if (!normalAttribute) {
normalAttribute = { size: 3, value: new Float32Array(vertexCount * 3).fill(0) };
}
let texCoordAttribute = attributes.TEXCOORD_0 || attributes.texCoords;
if (!texCoordAttribute) {
texCoordAttribute = { size: 2, value: new Float32Array(vertexCount * 2).fill(0) };
}
return {
positions: positionAttribute,
colors: colorAttribute,
normals: normalAttribute,
texCoords: texCoordAttribute
};
}
/*
* Convert mesh data into geometry
* @returns {Geometry} geometry
*/
function getGeometry(data) {
if (data instanceof Geometry) {
// @ts-expect-error data.attributes is readonly
data.attributes = normalizeGeometryAttributes(data.attributes);
return data;
}
else if (data.attributes) {
return new Geometry({
...data,
topology: 'triangle-list',
attributes: normalizeGeometryAttributes(data.attributes)
});
}
else {
return new Geometry({
topology: 'triangle-list',
attributes: normalizeGeometryAttributes(data)
});
}
}
const DEFAULT_COLOR = [0, 0, 0, 255];
const defaultProps = {
mesh: { type: 'object', value: null, async: true },
texture: { type: 'image', value: null, async: true },
sizeScale: { type: 'number', value: 1, min: 0 },
// _instanced is a hack to use world position instead of meter offsets in mesh
// TODO - formalize API
_instanced: true,
// NOTE(Tarek): Quick and dirty wireframe. Just draws
// the same mesh with LINE_STRIPS. Won't follow edges
// of the original mesh.
wireframe: false,
// Optional material for 'lighting' shader module
material: true,
getPosition: { type: 'accessor', value: (x) => x.position },
getColor: { type: 'accessor', value: DEFAULT_COLOR },
// yaw, pitch and roll are in degrees
// https://en.wikipedia.org/wiki/Euler_angles
// [pitch, yaw, roll]
getOrientation: { type: 'accessor', value: [0, 0, 0] },
getScale: { type: 'accessor', value: [1, 1, 1] },
getTranslation: { type: 'accessor', value: [0, 0, 0] },
// 4x4 matrix
getTransformMatrix: { type: 'accessor', value: [] },
textureParameters: { type: 'object', ignore: true, value: null }
};
/** Render a number of instances of an arbitrary 3D geometry. */
class SimpleMeshLayer extends Layer {
getShaders() {
return super.getShaders({
vs,
fs,
modules: [project32, phongMaterial, picking, simpleMeshUniforms]
});
}
getBounds() {
if (this.props._instanced) {
return super.getBounds();
}
let result = this.state.positionBounds;
if (result) {
return result;
}
const { mesh } = this.props;
if (!mesh) {
return null;
}
// @ts-ignore Detect if mesh is generated by loaders.gl
result = mesh.header?.boundingBox;
if (!result) {
// Otherwise, calculate bounding box from positions
const { attributes } = getGeometry(mesh);
attributes.POSITION = attributes.POSITION || attributes.positions;
//@ts-expect-error
result = getMeshBoundingBox(attributes);
}
this.state.positionBounds = result;
return result;
}
initializeState() {
const attributeManager = this.getAttributeManager();
// attributeManager is always defined in a primitive layer
attributeManager.addInstanced({
instancePositions: {
transition: true,
type: 'float64',
fp64: this.use64bitPositions(),
size: 3,
accessor: 'getPosition'
},
instanceColors: {
type: 'unorm8',
transition: true,
size: this.props.colorFormat.length,
accessor: 'getColor',
defaultValue: [0, 0, 0, 255]
},
instanceModelMatrix: MATRIX_ATTRIBUTES
});
this.setState({
// Avoid luma.gl's missing uniform warning
// TODO - add feature to luma.gl to specify ignored uniforms?
emptyTexture: this.context.device.createTexture({
data: new Uint8Array(4),
width: 1,
height: 1
})
});
}
updateState(params) {
super.updateState(params);
const { props, oldProps, changeFlags } = params;
if (props.mesh !== oldProps.mesh || changeFlags.extensionsChanged) {
this.state.positionBounds = null;
this.state.model?.destroy();
if (props.mesh) {
this.state.model = this.getModel(props.mesh);
const attributes = props.mesh.attributes || props.mesh;
this.setState({
hasNormals: Boolean(attributes.NORMAL || attributes.normals)
});
}
// attributeManager is always defined in a primitive layer
this.getAttributeManager().invalidateAll();
}
if (props.texture !== oldProps.texture && props.texture instanceof Texture) {
this.setTexture(props.texture);
}
if (this.state.model) {
this.state.model.setTopology(this.props.wireframe ? 'line-strip' : 'triangle-list');
}
}
finalizeState(context) {
super.finalizeState(context);
this.state.emptyTexture.delete();
}
draw({ uniforms }) {
const { model } = this.state;
if (!model) {
return;
}
const { viewport, renderPass } = this.context;
const { sizeScale, coordinateSystem, _instanced } = this.props;
const simpleMeshProps = {
sizeScale,
composeModelMatrix: !_instanced || shouldComposeModelMatrix(viewport, coordinateSystem),
flatShading: !this.state.hasNormals
};
model.shaderInputs.setProps({ simpleMesh: simpleMeshProps });
model.draw(renderPass);
}
get isLoaded() {
return Boolean(this.state?.model && super.isLoaded);
}
getModel(mesh) {
const model = new Model(this.context.device, {
...this.getShaders(),
id: this.props.id,
bufferLayout: this.getAttributeManager().getBufferLayouts(),
geometry: getGeometry(mesh),
isInstanced: true
});
const { texture } = this.props;
const { emptyTexture } = this.state;
const simpleMeshProps = {
sampler: texture || emptyTexture,
hasTexture: Boolean(texture)
};
model.shaderInputs.setProps({ simpleMesh: simpleMeshProps });
return model;
}
setTexture(texture) {
const { emptyTexture, model } = this.state;
// props.mesh may not be ready at this time.
// The sampler will be set when `getModel` is called
if (model) {
const simpleMeshProps = {
sampler: texture || emptyTexture,
hasTexture: Boolean(texture)
};
model.shaderInputs.setProps({ simpleMesh: simpleMeshProps });
}
}
}
SimpleMeshLayer.defaultProps = defaultProps;
SimpleMeshLayer.layerName = 'SimpleMeshLayer';
export default SimpleMeshLayer;
//# sourceMappingURL=simple-mesh-layer.js.map