目录
说在前面
- 操作系统:windows 11
- 浏览器:edge版本 124.0.2478.97
- recast-navigation-js版本:0.29.0
- golang版本:1.21.5
目录结构
shell
D:.
│ go.mod
│ go.sum
│ main.go // websocket server
└─public
│ index.html
└─js
mesh.js
websocket服务器
-
服务器使用
golang
+gin
+gorilla/websocket
实现,代码比较简单:gopackage main import ( "fmt" "log" "net/http" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) var upgrade = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } func main() { r := gin.Default() // nav mesh data // 这里只是举例,可以根据需求从文件读取或者生成 var mesh []byte // html static r.Static("/public", "./public") r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.GET("/ws", func(c *gin.Context) { // upgrade to websocket ws, err := upgrade.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Fatalln(err) } // release source defer ws.Close() go func() { // connect done <-c.Done() fmt.Println("ws lost connection") }() messageType, p, err := ws.ReadMessage() if err != nil { fmt.Println(err) return } switch messageType { case websocket.TextMessage: // Handle Text Message ws.WriteMessage(websocket.TextMessage, p) // c.Writer.Write(p) case websocket.BinaryMessage: // Handle Binary Data fmt.Println("handle binary data") mesh = p // 这里直接假定客户端传过来的是navmesh数据 ws.WriteMessage(websocket.BinaryMessage, mesh) case websocket.CloseMessage: // Websocket close fmt.Println("close websocket connection") return case websocket.PingMessage: // Websocket ping fmt.Println("ping") ws.WriteMessage(websocket.PongMessage, []byte("ping")) case websocket.PongMessage: // Websocket pong fmt.Println("pong") ws.WriteMessage(websocket.PongMessage, []byte("pong")) default: // Unhandled message type fmt.Printf("unknown message type: %d\n", messageType) return } }) r.Run() // listen and serve on 0.0.0.0:8080 }
前端
-
使用
three.js
绘制数据 -
index.html
html<html> <script type="importmap"> { "imports": { "@recast-navigation/core": "https://unpkg.com/@recast-navigation/core@0.29.0/dist/index.mjs", "@recast-navigation/wasm": "https://unpkg.com/@recast-navigation/wasm@0.29.0/dist/recast-navigation.wasm-compat.js", "@recast-navigation/generators": "https://unpkg.com/@recast-navigation/generators@0.29.0/dist/index.mjs", "@recast-navigation/three": "https://unpkg.com/@recast-navigation/three@0.29.0/dist/index.mjs", "three": "https://unpkg.com/three@0.164.0/build/three.module.js", "three/examples/jsm/controls/OrbitControls": "https://unpkg.com/three@0.164.0/examples/jsm/controls/OrbitControls.js" } } </script> <script src="./js/mesh.js" type="module" ></script> <style> body { margin: 0; overflow: hidden; } canvas { width: 100%; height: 100vh; } </style> </html>
-
mesh.js
主要流程- 在websocket建立连接成功后,将烘焙的
navmesh
数据发送至服务器 - 服务器在收到数据后直接返回(模拟通信过程,实际上服务器也可以从文件读取然后返回给前端)
- 前端接收到数据后通过
importNavMesh
接口初始化新的navmesh
- 使用
threejs
重新绘制新的navmesh
jsimport * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { init as initRecastNavigation, NavMeshQuery, } from '@recast-navigation/core'; import { generateSoloNavMesh } from '@recast-navigation/generators'; import { DebugDrawer, getPositionsAndIndices, } from '@recast-navigation/three'; import { exportNavMesh, importNavMesh } from '@recast-navigation/core'; // initialise recast-navigation await initRecastNavigation(); var ws = new WebSocket("ws://127.0.0.1:8080/ws"); ws.binaryType = "arraybuffer"; // use arraybuffer ws.onopen = function () { console.log("websocket connected."); meshInit(); }; ws.onmessage = function (e) { console.log("websockt data:", e); var uint8_msg = new Uint8Array(e.data); // convert to uint8 array meshDraw(uint8_msg); }; ws.onerror = function () { console.log("websocket error."); }; // setup scene const renderer = new THREE.WebGLRenderer(); document.body.appendChild(renderer.domElement); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(); camera.position.set(10, 10, -10); const orbitControls = new OrbitControls(camera, renderer.domElement); // add some meshes const ground = new THREE.Mesh( new THREE.BoxGeometry(10, 1, 10), new THREE.MeshBasicMaterial({ color: '#333' }) ); ground.position.set(0, -0.5, 0); scene.add(ground); const boxOne = new THREE.Mesh( new THREE.BoxGeometry(8, 2, 1), new THREE.MeshBasicMaterial({ color: '#555' }) ); boxOne.rotation.y = Math.PI / 4; boxOne.position.set(-2, 1, 0); scene.add(boxOne); const boxTwo = new THREE.Mesh( new THREE.BoxGeometry(8, 2, 1), new THREE.MeshBasicMaterial({ color: '#555' }) ); boxTwo.rotation.y = Math.PI / 4; boxTwo.position.set(2, 1, 0); scene.add(boxTwo); // get the positions and indices that we want to generate a navmesh from const [positions, indices] = getPositionsAndIndices([ ground, boxOne, boxTwo, ]); // generate a solo navmesh const cs = 0.05; const ch = 0.05; const walkableRadius = 0.2; const { success, navMesh } = generateSoloNavMesh(positions, indices, { cs, ch, walkableRadius: Math.round(walkableRadius / ch), }); // debug draw the navmesh const debugDrawer = new DebugDrawer(); debugDrawer.drawNavMesh(navMesh); scene.add(debugDrawer); // compute a path const start = { x: -4, y: 0, z: -4 }; const end = { x: 4, y: 0, z: 4 }; const navMeshQuery = new NavMeshQuery(navMesh); const { path } = navMeshQuery.computePath(start, end); // draw the path start const startMarker = new THREE.Mesh( new THREE.BoxGeometry(0.1, 0.1, 0.1), new THREE.MeshBasicMaterial({ color: 'blue' }) ); startMarker.position.set(start.x, start.y + 0.1, start.z); scene.add(startMarker); // draw the path end const endMarker = new THREE.Mesh( new THREE.BoxGeometry(0.1, 0.1, 0.1), new THREE.MeshBasicMaterial({ color: 'green' }) ); endMarker.position.set(end.x, end.y + 0.1, end.z); scene.add(endMarker); // draw the path line const line = new THREE.Line( new THREE.BufferGeometry().setFromPoints( path.map(({ x, y, z }) => new THREE.Vector3(x, y, z)) ), new THREE.LineBasicMaterial({ color: 'blue' }) ); line.position.y += 0.1; scene.add(line); // handle resizing const onWindowResize = () => { debugDrawer.resize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }; onWindowResize(); window.addEventListener('resize', onWindowResize); // animate const animate = () => { requestAnimationFrame(animate); renderer.render(scene, camera); }; animate(); // send nav mesh data to server function meshInit() { const navMeshExport = exportNavMesh(navMesh); ws.send(navMeshExport); } // rebuild nav mesh from server and draw function meshDraw(bin) { const tmpNavMesh = importNavMesh(bin); const tmpDebugDrawer = new DebugDrawer(); tmpDebugDrawer.drawNavMesh(tmpNavMesh.navMesh); tmpDebugDrawer.position.z += 10; scene.add(tmpDebugDrawer); }
- 在websocket建立连接成功后,将烘焙的
结果
- 左侧为使用服务器数据重绘的
navmesh