// 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 */ // Copyright (c) 2015 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import { Layer, project32, phongLighting, picking, log } from '@deck.gl/core'; import { Texture } from '@luma.gl/core'; import { Model, Geometry } from '@luma.gl/engine'; import { MATRIX_ATTRIBUTES, shouldComposeModelMatrix } from "../utils/matrix.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, phongLighting, picking] }); } 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; model.setUniforms(uniforms); model.setUniforms({ sizeScale, composeModelMatrix: !_instanced || shouldComposeModelMatrix(viewport, coordinateSystem), flatShading: !this.state.hasNormals }); 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; model.setBindings({ sampler: texture || emptyTexture }); model.setUniforms({ hasTexture: Boolean(texture) }); 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) { model.setBindings({ sampler: texture || emptyTexture }); model.setUniforms({ hasTexture: Boolean(texture) }); } } } SimpleMeshLayer.defaultProps = defaultProps; SimpleMeshLayer.layerName = 'SimpleMeshLayer'; export default SimpleMeshLayer;