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;