diff --git a/src/App.tsx b/src/App.tsx index b94b875..42240a4 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,88 @@ +import Layout from "./three/Layout"; +import { ThreeProvider } from "./three"; +import useLayoutGeometry from "./three/useLayoutGeometry"; +import NavMesh from "./three/NavMesh"; -import Layout from "./three/Layout" -import {ThreeProvider} from "./three"; +const wallConfig = { + wallThickness: 0.4, + width: 20, + length: 20, + wallHeight: 1, +}; + +const walls = [ + [ + [1, -9.75], + [1, 1.2], + ], + [ + [-4.6, 1.2], + [1.02, 1.2], + ], + [ + [-9.55, 1.2], + [-7.1, 1.2], + ], + [ + [4, -9.75], + [4, -4], + ], + [ + [4, -0.6], + [4, 0.75], + ], + [ + [4, 0.55], + [8.34, 0.55], + ], + [ + [-9.65, 8.5], + [-6.8, 8.5], + ], + [ + [-3.75, 8.5], + [2.5, 8.5], + ], + [ + [5.55, 8.5], + [8.34, 8.5], + ], + [ + [-9.9, -9.75], + [1.2, -9.75], + ], + [ + [3.8, -9.75], + [8.7, -9.75], + ], + [ + [-9.7, -9.75], + [-9.7, 8.7], + ], + [ + [8.5, -9.75], + [8.5, 8.7], + ], +].map( + ([start, end]) => + [ + [start[0], 0, start[1]], + [end[0], wallConfig.wallHeight, end[1]], + ] as [number[], number[]] +); const App = () => { - return - - ; + const geometry = useLayoutGeometry({ + ...wallConfig, + walls, + }); + + return ( + + + + + ); }; export default App; diff --git a/src/three/Layout.ts b/src/three/Layout.ts index 6e478cd..5376dd9 100644 --- a/src/three/Layout.ts +++ b/src/three/Layout.ts @@ -1,181 +1,97 @@ import * as THREE from "three"; -import { init } from "recast-navigation"; -import { threeToSoloNavMesh, NavMeshHelper } from "recast-navigation/three"; - -type Vector3Pair = [number[], number[]]; +import { useThree } from "."; +import { useEffect } from "react"; +import useWebGLRenderTarget from "./useWebGLRenderTarget"; + +interface LayoutProps { + geometry: THREE.BufferGeometry; + wallHeight: number; + width: number; + length: number; +} -const PLANE_THICKNESS = 0.4; +const vertexShader = ` +varying vec3 vPos; -class Walls extends THREE.BufferGeometry { - private planeArray: Vector3Pair[] = []; - private floorVertices: number[] = []; +void main() { + vPos = position; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); +} +`; - constructor() { - super(); - } +const fragmentShader = ` +uniform sampler2D map; +varying vec3 vPos; - private _updateGeometry(): void { - const planeVertices = this.planeArray.flatMap(([min, max]) => { - const rotationMat = new THREE.Matrix4().lookAt( - new THREE.Vector3(min[0], 0, min[2]), - new THREE.Vector3(max[0], 0, max[2]), - new THREE.Vector3(0, 1, 0) - ); - const quaternion = new THREE.Quaternion().setFromRotationMatrix( - rotationMat - ); +void main() { + vec2 vUv = vec2(vPos.x / 20.0 + 0.5, -vPos.z / 20.0 + 0.5); + float texelSize = 1.0 / 512.0; - const depth = new THREE.Vector2( - max[0] - min[0], - max[2] - min[2] - ).length(); - const height = max[1] - min[1]; - - const boxGeometry = new THREE.BoxGeometry(PLANE_THICKNESS, height, depth); - boxGeometry.applyQuaternion(quaternion); - boxGeometry.translate( - (max[0] + min[0]) / 2, - (max[1] + min[1]) / 2, - (max[2] + min[2]) / 2 - ); + float center = texture2D(map, vUv).r; + float left = texture2D(map, vUv + vec2(-texelSize, 0.0)).r; + float right = texture2D(map, vUv + vec2(texelSize, 0.0)).r; + float top = texture2D(map, vUv + vec2(0.0, texelSize)).r; + float bottom = texture2D(map, vUv + vec2(0.0, -texelSize)).r; - const vertices = [...boxGeometry.attributes.position.array]; - const index = [...(boxGeometry.getIndex()?.array || [])]; - const data = index.flatMap((index) => [ - vertices[index * 3], - vertices[index * 3 + 1], - vertices[index * 3 + 2], - ]); - return data; - }); + bool isEdge = (center > 0.0) && (abs(center - left) + abs(center - right) + abs(center - top) + abs(center - bottom) > 0.0); - this.setAttribute( - "position", - new THREE.BufferAttribute( - new Float32Array([...this.floorVertices.flat(), ...planeVertices]), - 3 - ) - ); - this.computeVertexNormals(); - } + vec3 color = vec3(1.0); - public setFloor(width: number, length: number) { - const floor = [ - [1, 0, 1], - [1, 0, -1], - [-1, 0, 1], - [1, 0, -1], - [-1, 0, -1], - [-1, 0, 1], - ].flatMap(([x, y, z]) => [(x * width) / 2, y, (z * length) / 2]); - this.floorVertices = floor; - this._updateGeometry(); + if (isEdge) { + color = vec3(0.8); } - public setPlane(data: Vector3Pair[]): void { - if (!data) return; - this.planeArray = data; - this._updateGeometry(); + gl_FragColor = vec4(color, 1.0); +} +`; + +class OutlineMaterial extends THREE.ShaderMaterial { + constructor(map: THREE.Texture) { + super({ + uniforms: { + map: { value: map }, + }, + vertexShader, + fragmentShader, + }); } } -import { useThree } from "."; -import { useEffect, useState } from "react"; - -const Layout = () => { +const Layout = ({ geometry, width, length, wallHeight }: LayoutProps) => { const { scene } = useThree(); - const [geometry] = useState(new Walls()); + const topView = useWebGLRenderTarget(); useEffect(() => { if (!scene) return; - const material = new THREE.MeshPhongMaterial({ color: "white" }); + const material = new OutlineMaterial(topView.texture); const mesh = new THREE.Mesh(geometry, material); + mesh.frustumCulled = false; scene.add(mesh); - return () => { - scene.remove(mesh); - }; - }, [scene, geometry]); - - useEffect(() => { - if (!geometry) return; - - geometry.setPlane([ - [ - [1, 0, -10], - [1, 1, 1.2], - ], - [ - [-4.6, 0, 1.2], - [1.02, 1, 1.2], - ], - [ - [-9.55, 0, 1.2], - [-7.1, 1, 1.2], - ], - [ - [4, 0, -10], - [4, 1, -4], - ], - [ - [4, 0, -0.6], - [4, 1, 0.55], - ], - [ - [4.25, 0, 0.55], - [8.34, 1, 0.55], - ], - [ - [-9.65, 0, 8.5], - [-6.8, 1, 8.5], - ], - [ - [-3.75, 0, 8.5], - [2.5, 1, 8.5], - ], - [ - [5.55, 0, 8.5], - [8.34, 1, 8.5], - ], - [ - [-9.65, 0, -9.75], - [1.02, 1, -9.75], - ], - [ - [4.25, 0, -9.75], - [8.34, 1, -9.75], - ], - [ - [-9.75, 0, -10], - [-9.75, 1, 8.7], - ], - [ - [8.5, 0, -10], - [8.5, 1, 8.7], - ], - ]); - geometry.setFloor(20, 20); - - let navMeshHelper: NavMeshHelper; - init().then(() => { - const { navMesh } = threeToSoloNavMesh([new THREE.Mesh(geometry)], { - ch: 1e-2, - cs: PLANE_THICKNESS + 1e-2, - walkableHeight: 1, - }); - if (navMesh) { - navMeshHelper = new NavMeshHelper({ navMesh }); - scene.add(navMeshHelper); - } + requestAnimationFrame(() => { + const topViewSene = new THREE.Scene(); + const topViewMaterial = new THREE.MeshBasicMaterial({ color: "white" }); + const topViewMesh = new THREE.Mesh(geometry, topViewMaterial); + topViewMesh.frustumCulled = false; + topViewSene.add(topViewMesh); + const camera = new THREE.OrthographicCamera( + -width / 2, + width / 2, + length / 2, + -length / 2, + 0, + wallHeight + ); + camera.position.set(0, wallHeight + 1e-3, 0); + camera.lookAt(new THREE.Vector3()); + topView.render(topViewSene, camera); }); return () => { - if (navMeshHelper) { - scene.remove(navMeshHelper); - } + scene.remove(mesh); }; - }, [geometry]); + }, [scene, geometry]); return null; }; diff --git a/src/three/NavMesh.ts b/src/three/NavMesh.ts new file mode 100644 index 0000000..4d93bf3 --- /dev/null +++ b/src/three/NavMesh.ts @@ -0,0 +1,51 @@ +import * as THREE from "three"; +import { init, NavMesh } from "recast-navigation"; +import { threeToSoloNavMesh, NavMeshHelper } from "recast-navigation/three"; + +import { useThree } from "."; +import { useEffect, useState } from "react"; + +interface NavMeshProps { + geometry: THREE.BufferGeometry; + wallThickness: number; +} + +export const useNavMesh = ({ geometry, wallThickness }: NavMeshProps) => { + const [navMesh, setNavMesh] = useState(); + useEffect(() => { + if (!geometry) return; + + init().then(() => { + const { navMesh } = threeToSoloNavMesh([new THREE.Mesh(geometry)], { + ch: 1e-2, + cs: wallThickness + 1e-2, + walkableHeight: 1, + }); + if (navMesh) { + setNavMesh(navMesh); + } + }); + }, [geometry, wallThickness]); + + return navMesh; +}; + +const NavMeshRenderer = ({ geometry, wallThickness }: NavMeshProps) => { + const { scene } = useThree(); + const navMesh = useNavMesh({ geometry, wallThickness }); + + useEffect(() => { + if (!navMesh) return; + + const navMeshHelper = new NavMeshHelper({ navMesh }); + scene.add(navMeshHelper); + + return () => { + scene.remove(navMeshHelper); + }; + }, [navMesh]); + + return null; +}; + +export default NavMeshRenderer; diff --git a/src/three/index.tsx b/src/three/index.tsx index d2a8034..a9e210f 100644 --- a/src/three/index.tsx +++ b/src/three/index.tsx @@ -35,9 +35,6 @@ const ThreeProvider: React.FC<{ children?: ReactNode }> = ({ children }) => { camera.position.set(0, 20, 20); controls.update(); - const light = new THREE.HemisphereLight(0xffffff, 0x0bbbbbb, 1); - scene.add(light); - const handleResize = () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); @@ -63,7 +60,14 @@ const ThreeProvider: React.FC<{ children?: ReactNode }> = ({ children }) => { return ( -
+
{children} ); diff --git a/src/three/useLayoutGeometry.ts b/src/three/useLayoutGeometry.ts new file mode 100644 index 0000000..ffb2d58 --- /dev/null +++ b/src/three/useLayoutGeometry.ts @@ -0,0 +1,115 @@ +import { useEffect, useState } from "react"; +import * as THREE from "three"; + +type Vector3Pair = [number[], number[]]; + +class Layout extends THREE.BufferGeometry { + private thickness: number = 0; + private planeArray: Vector3Pair[] = []; + private floorVertices: number[] = []; + + constructor() { + super(); + } + + private _updateGeometry(): void { + const planeVertices = this.planeArray.flatMap(([min, max]) => { + const rotationMat = new THREE.Matrix4().lookAt( + new THREE.Vector3(min[0], 0, min[2]), + new THREE.Vector3(max[0], 0, max[2]), + new THREE.Vector3(0, 1, 0) + ); + const quaternion = new THREE.Quaternion().setFromRotationMatrix( + rotationMat + ); + + const depth = new THREE.Vector2( + max[0] - min[0], + max[2] - min[2] + ).length(); + const height = max[1] - min[1]; + + const boxGeometry = new THREE.BoxGeometry(this.thickness, height, depth); + boxGeometry.applyQuaternion(quaternion); + boxGeometry.translate( + (max[0] + min[0]) / 2, + (max[1] + min[1]) / 2, + (max[2] + min[2]) / 2 + ); + + const vertices = [...boxGeometry.attributes.position.array]; + const index = [...(boxGeometry.getIndex()?.array || [])]; + const data = index.flatMap((index) => [ + vertices[index * 3], + vertices[index * 3 + 1], + vertices[index * 3 + 2], + ]); + return data; + }); + + this.setAttribute( + "position", + new THREE.BufferAttribute( + new Float32Array([...this.floorVertices.flat(), ...planeVertices]), + 3 + ) + ); + this.computeVertexNormals(); + } + + public setFloor(width: number, length: number) { + const floor = [ + [1, 0, 1], + [1, 0, -1], + [-1, 0, 1], + [1, 0, -1], + [-1, 0, -1], + [-1, 0, 1], + ].flatMap(([x, y, z]) => [(x * width) / 2, y, (z * length) / 2]); + this.floorVertices = floor; + this._updateGeometry(); + } + + public setPlane(data: Vector3Pair[]): void { + if (!data) return; + this.planeArray = data; + this._updateGeometry(); + } + + public setThickness(thickness: number) { + this.thickness = thickness; + this._updateGeometry(); + } +} + +interface useWallGeometryProps { + width: number; + length: number; + walls: Array; + wallThickness: number; +} + +const useLayoutGeometry = ({ + width, + length, + walls, + wallThickness, +}: useWallGeometryProps) => { + const [geometry] = useState(new Layout()); + + useEffect(() => { + geometry.setFloor(width, length); + }, [width, length]); + + useEffect(() => { + geometry.setPlane(walls); + }, [walls]); + + useEffect(() => { + geometry.setThickness(wallThickness); + }, [wallThickness]); + + return geometry; +}; + +export default useLayoutGeometry; diff --git a/src/three/useWebGLRenderTarget.ts b/src/three/useWebGLRenderTarget.ts new file mode 100644 index 0000000..e225139 --- /dev/null +++ b/src/three/useWebGLRenderTarget.ts @@ -0,0 +1,18 @@ +import * as THREE from "three"; +import { useState } from "react"; +import { useThree } from "."; + +const useWebGLRenderTarget = () => { + const { renderer } = useThree(); + const [renderTarget] = useState(new THREE.WebGLRenderTarget(512, 512)); + + const render = (scene: THREE.Scene, camera: THREE.Camera) => { + renderer.setRenderTarget(renderTarget); + renderer.render(scene, camera); + renderer.setRenderTarget(null); + }; + + return { render, texture: renderTarget.texture }; +}; + +export default useWebGLRenderTarget;