diff --git a/app/components/Map.tsx b/app/components/Map.tsx index a3b4d84..bb3359e 100644 --- a/app/components/Map.tsx +++ b/app/components/Map.tsx @@ -2,15 +2,26 @@ import { useEffect, useRef } from "react"; import { init } from "../three/utils/init"; -import { initSimple } from "../three/simple-example"; export function Map() { const divRef = useRef(null); + useEffect(() => { + let ignore = false; if (!divRef.current) return; - init(divRef.current); - //initSimple(divRef.current); + + if (!ignore) { + init(divRef.current); + } + + return () => { + ignore = true; + }; }, [divRef]); - return
; + return ( +
+
+
+ ); } diff --git a/app/three/simple-example.js b/app/three/simple-example.js deleted file mode 100644 index 064675f..0000000 --- a/app/three/simple-example.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as THREE from "three"; - -export function initSimple(container) { - const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 0.1, - 1000 - ); - - const renderer = new THREE.WebGLRenderer(); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.setAnimationLoop(animate); - container.appendChild(renderer.domElement); - - const geometry = new THREE.BoxGeometry(1, 1, 1); - const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); - const cube = new THREE.Mesh(geometry, material); - scene.add(cube); - - camera.position.z = 5; - - function animate() { - cube.rotation.x += 0.01; - cube.rotation.y += 0.01; - - renderer.render(scene, camera); - } -} diff --git a/app/three/utils/build-meshes.ts b/app/three/utils/build-meshes.ts index a3615c8..b7e66f0 100644 --- a/app/three/utils/build-meshes.ts +++ b/app/three/utils/build-meshes.ts @@ -2,12 +2,15 @@ import { BufferAttribute, BufferGeometry, DoubleSide, + FrontSide, Group, Mesh, - MeshBasicMaterial, MeshStandardMaterial, } from "three"; +import { uniforms } from "./uniforms"; +import { shader } from "./shader"; + import { fetchTriangleIndices } from "./fetch-triangle-indices"; import { fetchVertices } from "./fetch-vertices"; import { TRIANGLE_INDICES_URL, VERTICES_URL } from "../config"; @@ -39,17 +42,23 @@ async function buildMesh(layerData: MappedFeature) { geometry.computeVertexNormals(); geometry.computeBoundingBox(); - const material = new MeshStandardMaterial( - { - color: color, - metalness: 0.1, - roughness: 0.75, - flatShading: true, - side: DoubleSide, - // wireframe: false, - } - // this.uniforms.clipping - ); + const material = new MeshStandardMaterial({ + color: color, + metalness: 0.1, + roughness: 0.75, + flatShading: true, + side: FrontSide, + wireframe: false, + }); + + // material.onBeforeCompile = (materialShader) => { + // materialShader.uniforms.clippingLow = uniforms.clipping.clippingLow; + // materialShader.uniforms.clippingHigh = uniforms.clipping.clippingHigh; + // materialShader.uniforms.clippingScale = uniforms.clipping.clippingScale; + + // materialShader.vertexShader = shader.vertexMeshStandard; + // materialShader.fragmentShader = shader.fragmentClippingMeshStandard; + // }; const mesh = new Mesh(geometry, material); mesh.name = name; diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index ad66b13..e1f8d4a 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -41,7 +41,7 @@ export async function buildScene(container: HTMLElement, extent: Extent) { const width = container.clientWidth; const height = container.clientHeight; - camera = new PerspectiveCamera(30, width / height, 0.1, size * 100); + camera = new PerspectiveCamera(30, width / height, 0.1, size * 25); camera.position.set(center.x, center.y, size * 5); camera.lookAt(center); @@ -51,11 +51,12 @@ export async function buildScene(container: HTMLElement, extent: Extent) { renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); - //renderer.autoClear = false; - //renderer.setClearColor(0x000000, 0.0); // second param is opacity, 0 => transparent - - // enable clipping renderer.localClippingEnabled = true; + // renderer.autoClear = false; + // renderer.setClearColor(0x000000, 0.0); // second param is opacity, 0 => transparent + renderer.setAnimationLoop(animate); + window.addEventListener("resize", () => onWindowResize(container)); + container.appendChild(renderer.domElement); controls = new OrbitControls(camera, renderer.domElement); @@ -72,17 +73,6 @@ export async function buildScene(container: HTMLElement, extent: Extent) { // const urlParams = new URLSearchParams(queryString); // const modelid = parseInt(urlParams.get("model_id") ?? "20", 10); - renderer.setAnimationLoop(animate); - - window.addEventListener("resize", () => onWindowResize(container)); - - const testCube = new Mesh( - new BoxGeometry(size * 0.1, size * 0.1, size * 0.1), - new MeshBasicMaterial({ color: 0xff0000 }) - ); - testCube.position.copy(center); - scene.add(testCube); - return { renderer, scene, camera }; } @@ -99,6 +89,7 @@ function onWindowResize(container: HTMLElement) { function animate() { renderer.render(scene, camera); + // required if controls.enableDamping or controls.autoRotate are set to true controls.update(); } diff --git a/app/three/utils/fetch-triangle-indices.ts b/app/three/utils/fetch-triangle-indices.ts index d5198b2..d15bd55 100644 --- a/app/three/utils/fetch-triangle-indices.ts +++ b/app/three/utils/fetch-triangle-indices.ts @@ -1,5 +1,5 @@ -import { unpackEdges } from "./parsers"; import { request } from "./request"; +import { unpackEdges } from "./parsers"; export async function fetchTriangleIndices(edgeUrl: string, geomId: string) { const url = edgeUrl + geomId; diff --git a/app/three/utils/init.ts b/app/three/utils/init.ts index 8e28a67..690c996 100644 --- a/app/three/utils/init.ts +++ b/app/three/utils/init.ts @@ -21,8 +21,8 @@ export async function init(container: HTMLElement) { const { renderer, scene, camera } = await buildScene(container, extent); const meshes = await buildMeshes(mappedFeatures); - // const mappedFeaturesGroup = new Group(); - // mappedFeaturesGroup.add(...meshes); - // scene.add(mappedFeaturesGroup); - scene.add(meshes[0]); + const mappedFeaturesGroup = new Group(); + mappedFeaturesGroup.add(...meshes); + scene.add(mappedFeaturesGroup); + // scene.add(meshes[8]); } diff --git a/app/three/utils/parsers.ts b/app/three/utils/parsers.ts index 2299dd2..d064c08 100644 --- a/app/three/utils/parsers.ts +++ b/app/three/utils/parsers.ts @@ -1,9 +1,70 @@ +const METABYTES = 13; +export function unpackEdges(arrayBuffer: ArrayBuffer) { + const dv = new DataView(arrayBuffer, METABYTES); + const indices = new Uint32Array((arrayBuffer.byteLength - METABYTES) / 4); + for (let i = 0; i < indices.length; i++) { + indices[i] = dv.getUint32(i * 4, true); + } + return indices; +} + const DIMENSIONS = 3; const ONEBYTE = 1; -const FOURBYTE = 4; -const METABYTES = 13; +const FOURBYTE = 4; // bytes count for metadata in PG_pointcloud (significant bits compression) +export function unpackVertices(arrayBuffer: ArrayBuffer) { + const dataView = new DataView(arrayBuffer); + let ptr = ONEBYTE + 2 * FOURBYTE; + const pointsCount = dataView.getUint32(ptr, true); // 1 + 4 + 4 = 9 bytes offset + const posArray = new Float32Array(pointsCount * DIMENSIONS); + ptr += FOURBYTE; -class BitStream { + let bytesCount; + let significantBitsCount; + let commonBits; + let significantBits; + for (let dim = 0; dim < 3; dim++) { + ptr += ONEBYTE; + bytesCount = dataView.getInt32(ptr, true) - 8; + ptr += FOURBYTE; + significantBitsCount = dataView.getUint32(ptr, true); + ptr += FOURBYTE; + commonBits = readCommonBits(dataView, ptr); + ptr += FOURBYTE; + significantBits = readSignificantBits(dataView, ptr, bytesCount); + let value = 0.0; + for (var j = dim, i = 0; i < pointsCount; j += DIMENSIONS, i++) { + value = significantBits.readBits(significantBitsCount, 0) | commonBits; + if (dim === 2) { + value = value / 100; // z values in pc_patch from DB are multiplied by 100 + } + posArray[j] = value; + } + ptr += bytesCount; + } + return posArray; +} + +function readCommonBits(dataView: DataView, ptr: number) { + const temp = new Int32Array(1); + temp[0] = dataView.getInt32(ptr, false); + const combits = new BitStream(new Uint8Array(temp.buffer)); + return combits.readBits(32, 0); +} + +function readSignificantBits( + dataView: DataView, + ptr: number, + bytesCount: number +) { + const temp = new Int32Array(bytesCount / 4); + for (let i = ptr, j = 0; i < ptr + bytesCount; i += 4, j++) { + temp[j] = dataView.getInt32(i); + } + const sigbits = new BitStream(new Uint8Array(temp.buffer)); + return sigbits; +} + +export class BitStream { private a: Uint8Array; private position: number; private bitsPending: number; @@ -15,114 +76,67 @@ class BitStream { } writeBits(bits: number, value: number) { - if (bits === 0) return; + if (bits === 0) { + return; + } value &= 0xffffffff >>> (32 - bits); - - while (bits > 0) { - if (this.bitsPending === 0) { - this.a[this.position++] = 0; - this.bitsPending = 8; + let bitsConsumed; + if (this.bitsPending > 0) { + if (this.bitsPending > bits) { + this.a[this.position - 1] |= value << (this.bitsPending - bits); + bitsConsumed = bits; + this.bitsPending -= bits; + } else if (this.bitsPending === bits) { + this.a[this.position - 1] |= value; + bitsConsumed = bits; + this.bitsPending = 0; + } else { + this.a[this.position - 1] |= value >> (bits - this.bitsPending); + bitsConsumed = this.bitsPending; + this.bitsPending = 0; } - - const bitsToWrite = Math.min(this.bitsPending, bits); - const shift = this.bitsPending - bitsToWrite; - this.a[this.position - 1] |= (value >> (bits - bitsToWrite)) << shift; - - bits -= bitsToWrite; - value &= (1 << bits) - 1; - this.bitsPending -= bitsToWrite; + } else { + bitsConsumed = Math.min(8, bits); + this.bitsPending = 8 - bitsConsumed; + this.a[this.position++] = + (value >> (bits - bitsConsumed)) << this.bitsPending; + } + bits -= bitsConsumed; + if (bits > 0) { + this.writeBits(bits, value); } } - readBits(bits: number, bitBuffer = 0) { - if (bits === 0) return bitBuffer; - - while (bits > 0) { - if (this.bitsPending === 0) { - this.bitsPending = 8; - } - - const byte = this.a[this.position - (this.bitsPending === 8 ? 0 : 1)]; - const bitsToRead = Math.min(this.bitsPending, bits); - const shift = this.bitsPending - bitsToRead; - bitBuffer = - (bitBuffer << bitsToRead) | ((byte >> shift) & ((1 << bitsToRead) - 1)); - - bits -= bitsToRead; - this.bitsPending -= bitsToRead; - if (this.bitsPending === 0) this.position++; + readBits(bits: number, bitBuffer: number): number { + if (typeof bitBuffer === "undefined") { + bitBuffer = 0; } - - return bitBuffer; + if (bits === 0) { + return bitBuffer; + } + let partial; + let bitsConsumed; + if (this.bitsPending > 0) { + const byte = this.a[this.position - 1] & (0xff >> (8 - this.bitsPending)); + bitsConsumed = Math.min(this.bitsPending, bits); + this.bitsPending -= bitsConsumed; + partial = byte >> this.bitsPending; + } else { + bitsConsumed = Math.min(8, bits); + this.bitsPending = 8 - bitsConsumed; + partial = this.a[this.position++] >> this.bitsPending; + } + bits -= bitsConsumed; + bitBuffer = (bitBuffer << bitsConsumed) | partial; + return bits > 0 ? this.readBits(bits, bitBuffer) : bitBuffer; } seekTo(bitPos: number) { - this.position = Math.floor(bitPos / 8); - this.bitsPending = bitPos % 8 ? 8 - (bitPos % 8) : 0; - if (this.bitsPending > 0) this.position++; - } -} - -export function unpackVertices(arrayBuffer: ArrayBuffer) { - const dataView = new DataView(arrayBuffer); - let ptr = ONEBYTE + 2 * FOURBYTE; - - // Read the number of points - const pointsCount = dataView.getUint32(ptr, true); - ptr += FOURBYTE; - - // Initialize position array - const posArray = new Float32Array(pointsCount * DIMENSIONS); - - for (let dim = 0; dim < DIMENSIONS; dim++) { - ptr += ONEBYTE; // Skip unused byte - const bytesCount = dataView.getInt32(ptr, true) - 8; - ptr += FOURBYTE; - - const significantBitsCount = dataView.getUint32(ptr, true); - ptr += FOURBYTE; - - const commonBits = readCommonBits(dataView, ptr); - ptr += FOURBYTE; - - const significantBits = readSignificantBits(dataView, ptr, bytesCount); - ptr += bytesCount; - - // Read vertex data - for (let i = 0, j = dim; i < pointsCount; i++, j += DIMENSIONS) { - let value = significantBits.readBits(significantBitsCount) | commonBits; - if (dim === 2) value /= 100; // Adjust Z values - posArray[j] = value; + this.position = (bitPos / 8) | 0; + this.bitsPending = bitPos % 8; + if (this.bitsPending > 0) { + this.bitsPending = 8 - this.bitsPending; + this.position++; } } - - return posArray; -} - -export function unpackEdges(arrayBuffer: ArrayBuffer) { - const dv = new DataView(arrayBuffer, METABYTES); - const indices = new Uint32Array((arrayBuffer.byteLength - METABYTES) / 4); - for (let i = 0; i < indices.length; i++) { - indices[i] = dv.getUint32(i * 4, true); - } - return indices; -} - -function readSignificantBits( - dataView: DataView, - ptr: number, - bytesCount: number -) { - const temp = new Int32Array(bytesCount / 4); - for (let i = 0; i < temp.length; i++, ptr += 4) { - temp[i] = dataView.getInt32(ptr); - } - return new BitStream(new Uint8Array(temp.buffer)); -} - -function readCommonBits(dataView: DataView, ptr: number) { - const temp = new Int32Array(1); - temp[0] = dataView.getInt32(ptr, false); - const combits = new BitStream(new Uint8Array(temp.buffer)); - return combits.readBits(32); } diff --git a/app/three/utils/shader.ts b/app/three/utils/shader.ts new file mode 100644 index 0000000..d813645 --- /dev/null +++ b/app/three/utils/shader.ts @@ -0,0 +1,254 @@ +export const shader = { + vertex: ` + uniform vec3 color; + varying vec3 pixelNormal; + + void main() { + + pixelNormal = normal; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + }`, + + vertexClipping: ` + uniform vec3 color; + uniform vec3 clippingLow; + uniform vec3 clippingHigh; + + varying vec3 pixelNormal; + varying vec4 worldPosition; + varying vec3 camPosition; + varying vec3 vNormal; + varying vec3 vPosition; + varying vec2 vUv; + + void main() { + vUv = uv; + vec4 vPos = modelViewMatrix * vec4( position, 1.0 ); + vPosition = vPos.xyz; + + vNormal = normalMatrix * normal; + pixelNormal = normal; + worldPosition = modelMatrix * vec4( position, 1.0 ); + camPosition = cameraPosition; + + // gl_Position = projectionMatrix * vPos; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + }`, + + fragment: ` + uniform vec3 color; + varying vec3 pixelNormal; + + varying vec3 vNormal; + varying vec3 vPosition; + + // uniform vec3 spotLightPosition; // in world space + // uniform vec3 clight; + // uniform vec3 cspec; + // uniform float roughness; + const float PI = 3.14159; + + + void main( void ) { + + float shade = ( + 3.0 * pow ( abs ( pixelNormal.y ), 2.0 ) + + 2.0 * pow ( abs ( pixelNormal.z ), 2.0 ) + + 1.0 * pow ( abs ( pixelNormal.x ), 2.0 ) + ) / 3.0; + + gl_FragColor = vec4( color * shade, 1.0 ); + //gl_FragColor = vec4(color, 1.0); + + // vec4 lPosition = viewMatrix * vec4( spotLightPosition, 1.0 ); + // vec3 l = normalize(lPosition.xyz - vPosition.xyz); + // vec3 n = normalize( vNormal ); // interpolation destroys normalization, so we have to normalize + // vec3 v = normalize( -vPosition); + // vec3 h = normalize( v + l); + + // // small quantity to prevent divisions by 0 + // float nDotl = max(dot( n, l ),0.000001); + // float lDoth = max(dot( l, h ),0.000001); + // float nDoth = max(dot( n, h ),0.000001); + // float vDoth = max(dot( v, h ),0.000001); + // float nDotv = max(dot( n, v ),0.000001); + + // vec3 specularBRDF = FSchlick(lDoth)*GSmith(nDotv,nDotl)*DGGX(nDoth,roughness*roughness)/(4.0*nDotl*nDotv); + // vec3 outRadiance = (PI* clight * nDotl * specularBRDF); + + // gl_FragColor = vec4(pow( outRadiance, vec3(1.0/2.2)), 1.0); + + }`, + + fragmentClippingFront: ` + uniform sampler2D map; + uniform vec3 color; + uniform vec3 clippingLow; + uniform vec3 clippingHigh; + uniform float clippingScale; + uniform float percent; + + varying vec3 pixelNormal; + varying vec4 worldPosition; + varying vec3 camPosition; + varying vec2 vUv; + + void main( void ) { + + float shade = ( + 3.0 * pow ( abs ( pixelNormal.y ), 2.0 ) + + 2.0 * pow ( abs ( pixelNormal.z ), 2.0 ) + + 1.0 * pow ( abs ( pixelNormal.x ), 2.0 ) + ) / 3.0; + + if ( + worldPosition.x < clippingLow.x + || worldPosition.x > clippingHigh.x + || worldPosition.y < clippingLow.y + || worldPosition.y > clippingHigh.y + || worldPosition.z < (clippingLow.z * clippingScale) + || worldPosition.z > (clippingHigh.z * clippingScale) + ) { + discard; + } else { + gl_FragColor = texture2D(map, vUv); + gl_FragColor.a = percent; + } + + }`, + + vertexMeshStandard: ` + #define STANDARD + + varying vec3 vViewPosition; + varying vec4 worldPosition; + + #include + #include + #include + #include + #include + #include + #include + + void main() { + #include + #include + + #include + #include + + #include + #include + #include + #include + + vViewPosition = -mvPosition.xyz; + + worldPosition = modelMatrix * vec4(position, 1.0); + + #include + #include + } + `, + + fragmentClippingMeshStandard: ` + #define STANDARD + + uniform vec3 diffuse; + uniform vec3 emissive; + uniform float roughness; + uniform float metalness; + uniform float opacity; + + varying vec3 vViewPosition; + varying vec4 worldPosition; + + uniform vec3 clippingLow; + uniform vec3 clippingHigh; + uniform float clippingScale; + + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + + void main() { + #include + + vec4 diffuseColor = vec4(diffuse, opacity); + ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0)); + vec3 totalEmissiveRadiance = emissive; + + #ifdef TRANSMISSION + float totalTransmission = transmission; + float thicknessFactor = thickness; + #endif + + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + + vec3 rawDiffuseColor = diffuseColor.rgb; + #include + + // Lighting calculations + #include + #include + + // Ambient occlusion + #include + + vec3 outgoingLight = reflectedLight.directDiffuse + + reflectedLight.indirectDiffuse + + reflectedLight.directSpecular + + reflectedLight.indirectSpecular + + totalEmissiveRadiance; + + // Clipping logic + if (any(greaterThan(worldPosition.xyz, clippingHigh)) || any(lessThan(worldPosition.xyz, clippingLow))) { + discard; + } + + gl_FragColor = vec4(outgoingLight, diffuseColor.a); + } + `, + + invisibleVertexShader: ` + void main() { + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + gl_Position = projectionMatrix * mvPosition; + }`, + + invisibleFragmentShader: ` + void main( void ) { + gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + discard; + }`, +}; diff --git a/app/three/utils/uniforms.ts b/app/three/utils/uniforms.ts new file mode 100644 index 0000000..6d76b2e --- /dev/null +++ b/app/three/utils/uniforms.ts @@ -0,0 +1,19 @@ +import { Vector3, Color } from "three"; + +export const uniforms = { + clipping: { + // light blue + color: { value: new Color(0x3d9ecb) }, + clippingLow: { value: new Vector3(0, 0, 0) }, + clippingHigh: { value: new Vector3(0, 0, 0) }, + // additional parameter for scaling + clippingScale: { value: 1.0 }, + // topography + map: { value: null }, + percent: { value: 1 }, + }, + caps: { + // red + color: { value: new Color(0xf83610) }, + }, +};