Compare commits

..

5 commits
webgpu ... main

6 changed files with 215 additions and 192 deletions

View file

@ -14,10 +14,9 @@ import {
SceneViewContext, SceneViewContext,
SceneViewContextType, SceneViewContextType,
} from "../providers/scene-view-provider"; } from "../providers/scene-view-provider";
import { Mesh } from "three"; import { Mesh, MeshStandardMaterial } from "three";
import { CustomEvent } from "../three/SceneView"; import { CustomEvent } from "../three/SceneView";
import { RangeSlider } from "./RangeSlider"; import { RangeSlider } from "./RangeSlider";
import { MeshStandardNodeMaterial } from "three/webgpu";
function Toggle({ function Toggle({
title, title,
@ -264,10 +263,10 @@ export function Form() {
const key = `toggle-visibility-${child.name}`; const key = `toggle-visibility-${child.name}`;
let color = "transparent"; let color = "transparent";
if ( if (
(child as Mesh).material instanceof MeshStandardNodeMaterial (child as Mesh).material instanceof MeshStandardMaterial
) { ) {
color = `#${( color = `#${(
(child as Mesh).material as MeshStandardNodeMaterial (child as Mesh).material as MeshStandardMaterial
).color.getHexString()}`; ).color.getHexString()}`;
} }
const visible = (child as Mesh).visible; const visible = (child as Mesh).visible;

View file

@ -1,8 +1,10 @@
import { import {
Group, Group,
Material,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
MeshPhongMaterial, MeshPhongMaterial,
MeshStandardMaterial,
PerspectiveCamera, PerspectiveCamera,
Plane, Plane,
Raycaster, Raycaster,
@ -10,6 +12,7 @@ import {
SphereGeometry, SphereGeometry,
Vector2, Vector2,
Vector3, Vector3,
WebGLRenderer,
} from "three"; } from "three";
import { buildMeshes } from "./utils/build-meshes"; import { buildMeshes } from "./utils/build-meshes";
import { Extent, animate, buildScene } from "./utils/build-scene"; import { Extent, animate, buildScene } from "./utils/build-scene";
@ -36,11 +39,6 @@ import {
} from "geo-three"; } from "geo-three";
import { Data, createSVG } from "./utils/create-borehole-svg"; import { Data, createSVG } from "./utils/create-borehole-svg";
import { TileData, updateTiles } from "./ShaderMaterial"; import { TileData, updateTiles } from "./ShaderMaterial";
import {
ClippingGroup,
MeshStandardNodeMaterial,
WebGPURenderer,
} from "three/webgpu";
export type CustomEvent = CustomEventInit<{ export type CustomEvent = CustomEventInit<{
element: SVGSVGElement | null; element: SVGSVGElement | null;
@ -48,7 +46,7 @@ export type CustomEvent = CustomEventInit<{
export class SceneView extends EventTarget { export class SceneView extends EventTarget {
private _scene: Scene; private _scene: Scene;
private _model: ClippingGroup; private _model: Group;
private _camera: PerspectiveCamera; private _camera: PerspectiveCamera;
private _container: HTMLElement; private _container: HTMLElement;
private _raycaster: Raycaster; private _raycaster: Raycaster;
@ -60,17 +58,17 @@ export class SceneView extends EventTarget {
private _callback: EventListenerOrEventListenerObject | null = null; private _callback: EventListenerOrEventListenerObject | null = null;
private _orbitControls: OrbitControls; private _orbitControls: OrbitControls;
private _dragControls: DragControls | null = null; private _dragControls: DragControls | null = null;
private _renderer: WebGPURenderer; private _renderer: WebGLRenderer;
private static _DISPLACEMENT = 2000; private static _DISPLACEMENT = 2000;
constructor( constructor(
scene: Scene, scene: Scene,
model: ClippingGroup, model: Group,
camera: PerspectiveCamera, camera: PerspectiveCamera,
container: HTMLElement, container: HTMLElement,
extent: Extent, extent: Extent,
orbitControls: OrbitControls, orbitControls: OrbitControls,
renderer: WebGPURenderer renderer: WebGLRenderer
) { ) {
super(); super();
this._scene = scene; this._scene = scene;
@ -153,7 +151,7 @@ export class SceneView extends EventTarget {
// Set wireframe for model // Set wireframe for model
const model = this._model; const model = this._model;
model.children.forEach((child) => { model.children.forEach((child) => {
const material = (child as Mesh).material as MeshStandardNodeMaterial; const material = (child as Mesh).material as MeshStandardMaterial;
material.wireframe = !material.wireframe; material.wireframe = !material.wireframe;
}); });
@ -164,7 +162,7 @@ export class SceneView extends EventTarget {
if (capMeshGroup) { if (capMeshGroup) {
capMeshGroup.children.forEach((mesh) => { capMeshGroup.children.forEach((mesh) => {
const material = (mesh as Mesh).material as MeshStandardNodeMaterial; const material = (mesh as Mesh).material as MeshStandardMaterial;
if (material) { if (material) {
material.wireframe = !material.wireframe; material.wireframe = !material.wireframe;
} }
@ -215,7 +213,7 @@ export class SceneView extends EventTarget {
const depthEnd = intersects[i + 1].point.z; const depthEnd = intersects[i + 1].point.z;
const name = intersects[i].object.name; const name = intersects[i].object.name;
const color = `#${( const color = `#${(
(intersects[i].object as Mesh).material as MeshStandardNodeMaterial (intersects[i].object as Mesh).material as MeshStandardMaterial
).color.getHexString()}`; ).color.getHexString()}`;
// Avoid duplicate entries, just update the depth information // Avoid duplicate entries, just update the depth information
@ -341,9 +339,10 @@ export class SceneView extends EventTarget {
// Remove existing cap meshes // Remove existing cap meshes
for (const o in Orientation) { for (const o in Orientation) {
const capMeshGroupName = `cap-mesh-group-${o}`; const capMeshGroupName = `cap-mesh-group-${o}`;
const capMeshGroup = this._scene.getObjectByName(capMeshGroupName); let capMeshGroup = this._scene.getObjectByName(capMeshGroupName);
if (capMeshGroup) { while (capMeshGroup) {
capMeshGroup.clear(); this._scene.remove(capMeshGroup);
capMeshGroup = this._scene.getObjectByName(capMeshGroupName);
} }
} }
@ -353,8 +352,10 @@ export class SceneView extends EventTarget {
this._dragControls = null; this._dragControls = null;
} }
// Disable clipping group // Remove clipping planes
this.model.enabled = false; for (const mesh of this._model.children) {
((mesh as Mesh).material as Material).clippingPlanes = null;
}
} }
// Reset clipping box // Reset clipping box
@ -372,9 +373,10 @@ export class SceneView extends EventTarget {
this._dragControls = dragControls; this._dragControls = dragControls;
// Add planes to ClippingGroup // Add clipping planes to the meshes
this.model.clippingPlanes = planes; for (const mesh of this._model.children) {
this.model.enabled = true; ((mesh as Mesh).material as Material).clippingPlanes = planes;
}
} }
// Explode meshes // Explode meshes
@ -398,7 +400,7 @@ export class SceneView extends EventTarget {
this._resetClippingBox(); this._resetClippingBox();
} }
for (let i = 1; i < this._model.children.length; i++) { for (let i = 0; i < this._model.children.length; i++) {
const mesh = this._model.children[i]; const mesh = this._model.children[i];
if (explode) { if (explode) {
@ -450,13 +452,14 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
const { renderer, scene, camera, controls } = buildScene(container, extent); const { renderer, scene, camera, controls } = buildScene(container, extent);
// Start render loop
renderer.setAnimationLoop(animate(() => {}));
// Build the 3D model // Build the 3D model
const meshes = await buildMeshes(mappedFeatures); const model = new Group();
const model = new ClippingGroup();
model.enabled = false;
model.add(...meshes);
model.name = "geologic-model"; model.name = "geologic-model";
scene.add(model); scene.add(model);
await buildMeshes(mappedFeatures, model);
// Add a coordinate grid to the scene // Add a coordinate grid to the scene
const { gridHelper, annotations } = buildCoordinateGrid(extent); const { gridHelper, annotations } = buildCoordinateGrid(extent);
@ -488,14 +491,11 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
map.visible = false; map.visible = false;
scene.add(map); scene.add(map);
// Update render loop to include topography
const topography = scene.getObjectByName("Topography") as Mesh; const topography = scene.getObjectByName("Topography") as Mesh;
if (topography) { renderer.setAnimationLoop(
renderer.setAnimationLoop( animate(rendererCallback(camera, renderer, scene, map, extent, topography))
animate( );
rendererCallback(camera, renderer, scene, map, extent, topography)
)
);
}
return { return {
scene, scene,
@ -509,15 +509,14 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
function rendererCallback( function rendererCallback(
camera: PerspectiveCamera, camera: PerspectiveCamera,
renderer: WebGPURenderer, renderer: WebGLRenderer,
scene: Scene, scene: Scene,
map: MapView, map: MapView,
extent: Extent, extent: Extent,
topography: Mesh topography: Mesh | undefined
) { ) {
return () => { return () => {
if (topography.visible) { if (topography && topography.visible) {
//@ts-expect-error WebGPURenderer is not supported by Geo-Three
map.lod.updateLOD(map, camera, renderer, scene); map.lod.updateLOD(map, camera, renderer, scene);
const tiles: TileData[] = []; const tiles: TileData[] = [];
traverse(map.root, extent, tiles); traverse(map.root, extent, tiles);

View file

@ -1,24 +1,13 @@
import { import {
Color,
DataArrayTexture, DataArrayTexture,
LinearFilter, LinearFilter,
RGBAFormat, RGBAFormat,
SRGBColorSpace, ShaderChunk,
ShaderMaterial,
Texture, Texture,
Vector4, Vector4,
} from "three"; } from "three";
import {
Break,
Fn,
If,
Loop,
oneMinus,
positionWorld,
texture,
uniform,
uniformArray,
vec4,
} from "three/tsl";
import { MeshStandardNodeMaterial } from "three/webgpu";
export interface TileData { export interface TileData {
xmin: number; xmin: number;
@ -37,9 +26,9 @@ const height = 256;
const size = width * height; const size = width * height;
const canvas = new OffscreenCanvas(width, height); const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d", { willReadFrequently: true }); const ctx = canvas.getContext("2d");
const tileBounds: Vector4[] = Array(maxTiles).fill(new Vector4(0, 0, 0, 0)); const tileBounds = Array(maxTiles).fill(new Vector4(0, 0, 0, 0));
const data = new Uint8Array(4 * size * maxTiles); const data = new Uint8Array(4 * size * maxTiles);
const tileCache: { const tileCache: {
@ -53,65 +42,92 @@ dataArrayTexture.format = RGBAFormat;
dataArrayTexture.generateMipmaps = false; dataArrayTexture.generateMipmaps = false;
dataArrayTexture.magFilter = LinearFilter; dataArrayTexture.magFilter = LinearFilter;
dataArrayTexture.minFilter = LinearFilter; dataArrayTexture.minFilter = LinearFilter;
dataArrayTexture.colorSpace = SRGBColorSpace;
dataArrayTexture.needsUpdate = true; dataArrayTexture.needsUpdate = true;
// Create shader material // Create shader material
export const topoNodeMaterial = new MeshStandardNodeMaterial({ export const shaderMaterial = new ShaderMaterial({
alphaToCoverage: true, clipping: true,
}); uniforms: {
const tileBoundsUniform = uniformArray(tileBounds); tileBounds: { value: tileBounds },
const dataArrayTextureUniform = uniform(dataArrayTexture); tileCount: { value: maxTiles },
tiles: { value: dataArrayTexture },
color: { value: new Color(1, 1, 1) },
},
vertexShader:
ShaderChunk.common +
"\n" +
ShaderChunk.logdepthbuf_pars_vertex +
`
varying vec3 vWorldPosition;
varying float fragDepth;
const fragmentShader = /*#__PURE__*/ Fn(() => { #include <clipping_planes_pars_vertex>
const color = vec4(191.0 / 255.0, 209.0 / 255.0, 229.0 / 255.0, 1.0).toVar();
Loop({ start: 0, end: maxTiles, condition: "<" }, ({ i }) => {
const bounds = tileBoundsUniform.element(i);
If( void main() {
positionWorld.x #include <begin_vertex>
.greaterThanEqual(bounds.x) vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
.and(positionWorld.x.lessThanEqual(bounds.y)) gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
.and(positionWorld.y.greaterThanEqual(bounds.z)) fragDepth = (gl_Position.z / gl_Position.w + 1.0) * 0.5;
.and(positionWorld.y.lessThanEqual(bounds.w)),
() => {
const uv = positionWorld.xy
.sub(bounds.xz)
.div(bounds.yw.sub(bounds.xz))
.toVar();
uv.y.assign(oneMinus(uv.y));
const tile = texture(dataArrayTextureUniform.value, uv); #include <project_vertex>
color.assign(tile.depth(i)); #include <clipping_planes_vertex>
Break();
} ` +
); ShaderChunk.logdepthbuf_vertex +
}); `
}
`,
fragmentShader:
ShaderChunk.logdepthbuf_pars_fragment +
`
uniform vec4 tileBounds[${maxTiles}];
uniform int tileCount;
uniform sampler2DArray tiles;
varying vec3 vWorldPosition;
varying float fragDepth;
return color; #include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
vec4 color = vec4(191.0/255.0, 209.0/255.0, 229.0/255.0, 1.0); // Default color
for (int i = 0; i < ${maxTiles}; i++) {
if (i >= tileCount) break; // Only process available tiles
vec4 bounds = tileBounds[i];
if (vWorldPosition.x >= bounds.x && vWorldPosition.x <= bounds.y &&
vWorldPosition.y >= bounds.z && vWorldPosition.y <= bounds.w) {
vec2 uv = (vWorldPosition.xy - bounds.xz) / (bounds.yw - bounds.xz);
uv = vec2(uv.x, 1.0 - uv.y);
color = texture2D(tiles, vec3(uv, i));
break; // Stop checking once we find the correct tile
}
}
gl_FragColor = color;
gl_FragDepth = fragDepth;
` +
ShaderChunk.logdepthbuf_fragment +
`
}
`,
}); });
topoNodeMaterial.colorNode = fragmentShader();
let oldKeys: string[] = [];
export function updateTiles(newTiles: TileData[]) { export function updateTiles(newTiles: TileData[]) {
if (newTiles.length > maxTiles) { if (newTiles.length > maxTiles) {
newTiles = newTiles.slice(0, maxTiles); newTiles = newTiles.slice(0, maxTiles);
} }
const newKeys = newTiles.map((t) => getTileDataKey(t)); for (let i = 0; i < newTiles.length; i++) {
const update = updateDataArrayTexture(newTiles[i], i);
oldKeys.some((k, i) => k !== newKeys[i]) || oldKeys.length === 0;
// Only update if tiles changed
if (update) {
for (let i = 0; i < newTiles.length; i++) {
updateDataArrayTexture(newTiles[i], i);
}
dataArrayTexture.needsUpdate = true;
oldKeys = newKeys;
} }
dataArrayTexture.needsUpdate = true;
} }
// Update buffer // Update buffer

View file

@ -8,6 +8,8 @@ import {
LineBasicMaterial, LineBasicMaterial,
LineSegments, LineSegments,
Mesh, Mesh,
MeshBasicMaterial,
MeshStandardMaterial,
Object3DEventMap, Object3DEventMap,
PerspectiveCamera, PerspectiveCamera,
Plane, Plane,
@ -15,17 +17,11 @@ import {
Scene, Scene,
Vector2, Vector2,
Vector3, Vector3,
WebGLRenderer,
} from "three"; } from "three";
import { DragControls, OrbitControls } from "three/examples/jsm/Addons.js"; import { DragControls, OrbitControls } from "three/examples/jsm/Addons.js";
import { Extent } from "./build-scene"; import { Extent } from "./build-scene";
import earcut from "earcut"; import earcut from "earcut";
import {
ClippingGroup,
MeshBasicNodeMaterial,
MeshStandardNodeMaterial,
WebGPURenderer,
} from "three/webgpu";
import { color } from "three/tsl";
export enum Orientation { export enum Orientation {
X = "X", X = "X",
@ -36,7 +32,7 @@ export enum Orientation {
NZ = "NZ", NZ = "NZ",
} }
type PlaneMesh = Mesh<PlaneGeometry, MeshBasicNodeMaterial, Object3DEventMap>; type PlaneMesh = Mesh<PlaneGeometry, MeshBasicMaterial, Object3DEventMap>;
type EdgeMesh = LineSegments< type EdgeMesh = LineSegments<
EdgesGeometry<PlaneGeometry>, EdgesGeometry<PlaneGeometry>,
LineBasicMaterial, LineBasicMaterial,
@ -53,7 +49,7 @@ let currentExtent: Extent;
const BUFFER = 500; const BUFFER = 500;
export function buildClippingplanes( export function buildClippingplanes(
renderer: WebGPURenderer, renderer: WebGLRenderer,
camera: PerspectiveCamera, camera: PerspectiveCamera,
orbitControls: OrbitControls, orbitControls: OrbitControls,
extent: Extent, extent: Extent,
@ -152,7 +148,7 @@ export function buildClippingplanes(
const planeGeometry = new PlaneGeometry(width, height); const planeGeometry = new PlaneGeometry(width, height);
const planeMesh = new Mesh( const planeMesh = new Mesh(
planeGeometry, planeGeometry,
new MeshBasicNodeMaterial({ new MeshBasicMaterial({
visible: true, visible: true,
color: 0xa92a4e, color: 0xa92a4e,
transparent: true, transparent: true,
@ -194,25 +190,6 @@ export function buildClippingplanes(
edgeMeshMap[p.orientation] = edges; edgeMeshMap[p.orientation] = edges;
} }
for (const p of planesData) {
// Create ClippingGroup for each cap mesh face
const capMeshGroupName = `cap-mesh-group-${p.orientation}`;
let capMeshGroup = scene.getObjectByName(capMeshGroupName) as ClippingGroup;
if (capMeshGroup) {
capMeshGroup.clear();
} else {
capMeshGroup = new ClippingGroup();
capMeshGroup.name = capMeshGroupName;
scene.add(capMeshGroup);
}
// Set clipping planes for the cap meshes
const capMeshGroupPlanes = planes.filter(
(plane) => plane.normal.dot(p.normal) !== 1
);
capMeshGroup.clippingPlanes = capMeshGroupPlanes;
}
// Add meshes to the scene // Add meshes to the scene
const planeMeshGroup = new Group(); const planeMeshGroup = new Group();
planeMeshGroup.name = "clipping-planes"; planeMeshGroup.name = "clipping-planes";
@ -411,21 +388,29 @@ export function buildClippingplanes(
} }
// Remove existing cap meshes // Remove existing cap meshes
const capMeshGroupName = `cap-mesh-group-${orientation}`; const capMeshGroupName = `cap-mesh-group-${object.name}`;
const capMeshGroup = scene.getObjectByName( let capMeshGroup = scene.getObjectByName(capMeshGroupName);
capMeshGroupName while (capMeshGroup) {
) as ClippingGroup; scene.remove(capMeshGroup);
if (capMeshGroup) { capMeshGroup = scene.getObjectByName(capMeshGroupName);
capMeshGroup.clear(); }
// Generate new cap meshes // Generate new cap meshes
generateCapMeshes( const capMeshes = generateCapMeshes(
meshes, meshes,
plane.clone(), plane.clone(),
orientation, planes,
scene, orientation,
capMeshGroup scene
); );
// Add new cap meshes
if (capMeshes.length > 0) {
const newCapMeshGroup = new Group();
newCapMeshGroup.add(...capMeshes);
newCapMeshGroup.name = capMeshGroupName;
scene.add(newCapMeshGroup);
} }
}); });
@ -591,10 +576,12 @@ function resizeClippingPlane(
function generateCapMeshes( function generateCapMeshes(
meshes: Mesh[], meshes: Mesh[],
plane: Plane, plane: Plane,
planes: Plane[],
orientation: Orientation, orientation: Orientation,
scene: Scene, scene: Scene
capMeshGroup: ClippingGroup
) { ) {
const capMeshes: Mesh[] = [];
// Rescale to local coordinates // Rescale to local coordinates
if (orientation === Orientation.Z || orientation === Orientation.NZ) if (orientation === Orientation.Z || orientation === Orientation.NZ)
plane.constant /= scene.scale.z; plane.constant /= scene.scale.z;
@ -653,32 +640,60 @@ function generateCapMeshes(
// Intersection surface can be a multipolygon consisting of disconnected polygons // Intersection surface can be a multipolygon consisting of disconnected polygons
const polygons: Vector3[][] = buildPolygons(edges); const polygons: Vector3[][] = buildPolygons(edges);
const colorThree = // Clip cap surfaces with clipping planes
mesh.material instanceof MeshStandardNodeMaterial const clippingPlanes = planes.filter((p) => !p.normal.equals(plane.normal));
? mesh.material.color
: new Color(0.1, 0.1, 0.1);
const material = new MeshStandardNodeMaterial({ const offset =
orientation === Orientation.NX ||
orientation === Orientation.NY ||
orientation === Orientation.NZ
? 1
: -1;
const color =
mesh.material instanceof MeshStandardMaterial
? mesh.material.color
: new Color(1, 1, 1);
const material = new MeshStandardMaterial({
color,
side: DoubleSide, side: DoubleSide,
metalness: 0.0,
roughness: 1.0,
flatShading: true, flatShading: true,
polygonOffset: true,
polygonOffsetFactor: offset,
polygonOffsetUnits: offset,
clippingPlanes,
wireframe: scene.userData.wireframe, wireframe: scene.userData.wireframe,
alphaToCoverage: true,
}); });
material.colorNode = color(colorThree.r, colorThree.g, colorThree.b); const localMeshes = polygons.map((polygon) => {
polygons.forEach((polygon) => {
const geometry = triangulatePolygon(polygon, plane); const geometry = triangulatePolygon(polygon, plane);
const capMesh = new Mesh(geometry, material); const capMesh = new Mesh(geometry, material);
capMesh.visible = mesh.visible; capMesh.visible = mesh.visible;
capMesh.name = mesh.name; capMesh.name = mesh.name;
if (capMesh && geometry.index && geometry.index.count > 0) { // Offset mesh to avoid flickering
capMeshGroup.add(capMesh); const normal = plane.normal.clone().multiplyScalar(offset);
const positionAttr = capMesh.geometry.attributes.position;
for (let i = 0; i < positionAttr.count; i++) {
const x = positionAttr.getX(i) + normal.x;
const y = positionAttr.getY(i) + normal.y;
const z = positionAttr.getZ(i) + normal.z;
positionAttr.setXYZ(i, x, y, z);
} }
positionAttr.needsUpdate = true;
return capMesh;
}); });
capMeshes.push(...localMeshes);
} }
return capMeshes;
} }
// Build polygons by grouping connected intersection edges // Build polygons by grouping connected intersection edges

View file

@ -1,10 +1,15 @@
import { BufferAttribute, BufferGeometry, DoubleSide, Mesh } from "three"; import {
BufferAttribute,
BufferGeometry,
DoubleSide,
Group,
Mesh,
MeshStandardMaterial,
} from "three";
import { fetchVertices, fetchTriangleIndices, transform } from "./utils"; import { fetchVertices, fetchTriangleIndices, transform } from "./utils";
import { TRIANGLE_INDICES_URL, VERTICES_URL } from "../config"; import { TRIANGLE_INDICES_URL, VERTICES_URL } from "../config";
import { topoNodeMaterial } from "../ShaderMaterial"; import { shaderMaterial } from "../ShaderMaterial";
import { MeshStandardNodeMaterial } from "three/webgpu";
import { color } from "three/tsl";
interface MappedFeature { interface MappedFeature {
featuregeom_id: number; featuregeom_id: number;
@ -13,24 +18,22 @@ interface MappedFeature {
preview: { legend_color: string; legend_text: string }; preview: { legend_color: string; legend_text: string };
} }
export async function buildMeshes(mappedFeatures: MappedFeature[]) { export async function buildMeshes(
const meshes = []; mappedFeatures: MappedFeature[],
for (let i = 0; i < mappedFeatures.length; i++) { model: Group
const layerData = mappedFeatures[i]; ) {
const mesh = await buildMesh(layerData); for (const mappedFeature of mappedFeatures) {
if (layerData.name === "Topography") { const mesh = await buildMesh(mappedFeature);
if (mappedFeature.name === "Topography") {
mesh.visible = false; mesh.visible = false;
} else {
mesh.visible = true;
} }
meshes.push(mesh);
}
return meshes; model.add(mesh);
}
} }
async function buildMesh(layerData: MappedFeature) { async function buildMesh(layerData: MappedFeature) {
const colorHex = `#${layerData.preview.legend_color}`; const color = `#${layerData.preview.legend_color}`;
const name = layerData.preview.legend_text; const name = layerData.preview.legend_text;
const geomId = layerData.featuregeom_id.toString(); const geomId = layerData.featuregeom_id.toString();
@ -56,23 +59,19 @@ async function buildMesh(layerData: MappedFeature) {
const indices = new BufferAttribute(indexArray, 1); const indices = new BufferAttribute(indexArray, 1);
geometry.setIndex(indices); geometry.setIndex(indices);
geometry.computeVertexNormals();
const material = new MeshStandardNodeMaterial({ const material = new MeshStandardMaterial({
color: colorHex, color: color,
metalness: 0.1, metalness: 0.1,
roughness: 0.5, roughness: 0.5,
flatShading: true, flatShading: true,
side: DoubleSide, side: DoubleSide,
alphaToCoverage: true, wireframe: false,
}); });
// Required by ClippingGroup otherwise clipping does not work
material.colorNode = color(colorHex);
const mesh = new Mesh( const mesh = new Mesh(
geometry, geometry,
name === "Topography" ? topoNodeMaterial : material name === "Topography" ? shaderMaterial : material
); );
mesh.name = name; mesh.name = name;
mesh.userData.layerId = geomId; mesh.userData.layerId = geomId;

View file

@ -1,6 +1,7 @@
import { import {
PerspectiveCamera, PerspectiveCamera,
Scene, Scene,
WebGLRenderer,
AmbientLight, AmbientLight,
DirectionalLight, DirectionalLight,
Group, Group,
@ -18,7 +19,6 @@ import {
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { getCenter3D, getMaxSize } from "./utils"; import { getCenter3D, getMaxSize } from "./utils";
import { WebGPURenderer } from "three/webgpu";
export interface Extent { export interface Extent {
xmin: number; xmin: number;
@ -30,7 +30,7 @@ export interface Extent {
} }
let controls: OrbitControls; let controls: OrbitControls;
let renderer: WebGPURenderer; let renderer: WebGLRenderer;
let camera: PerspectiveCamera; let camera: PerspectiveCamera;
let scene: Scene; let scene: Scene;
let overlayCamera: OrthographicCamera; let overlayCamera: OrthographicCamera;
@ -54,15 +54,15 @@ export function buildScene(container: HTMLElement, extent: Extent) {
camera.lookAt(center); camera.lookAt(center);
// Initialize the renderer // Initialize the renderer
renderer = new WebGPURenderer({ renderer = new WebGLRenderer({
logarithmicDepthBuffer: true, logarithmicDepthBuffer: true,
antialias: true,
}); });
renderer.setPixelRatio(window.devicePixelRatio); renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height); renderer.setSize(width, height);
renderer.localClippingEnabled = true; renderer.localClippingEnabled = true;
renderer.autoClear = false; renderer.autoClear = false;
// renderer.setAnimationLoop(animate);
// Handle window resize event to adapt the aspect ratio // Handle window resize event to adapt the aspect ratio
window.addEventListener("resize", () => onWindowResize(container)); window.addEventListener("resize", () => onWindowResize(container));
@ -117,7 +117,7 @@ function onWindowResize(container: HTMLElement) {
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight); renderer.setSize(container.clientWidth, container.clientHeight);
// Required if controls.enableDamping or controls.autoRotate are set to true // required if controls.enableDamping or controls.autoRotate are set to true
controls.update(); controls.update();
} }
@ -152,12 +152,7 @@ function renderOverlay() {
); );
// Render the overlay scene to the screen (position it in the bottom left) // Render the overlay scene to the screen (position it in the bottom left)
renderer.setViewport( renderer.setViewport(10, 10, UI_WIDTH, UI_HEIGHT);
10,
renderer.domElement.height - UI_HEIGHT - 10,
UI_WIDTH,
UI_HEIGHT
);
renderer.render(overlayScene, overlayCamera); renderer.render(overlayScene, overlayCamera);
renderer.setViewport( renderer.setViewport(
0, 0,
@ -184,7 +179,7 @@ function buildDefaultLights(scene: Scene, extent: Extent) {
lights.push(ambient); lights.push(ambient);
// Directional lights // Directional lights
const directionalLight = new DirectionalLight(0xffffff, 2); const directionalLight = new DirectionalLight(0xffffff, 1.5);
directionalLight.position.set( directionalLight.position.set(
lightPosition.x, lightPosition.x,
lightPosition.y, lightPosition.y,