minecraft-renderer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +297 -0
  2. package/dist/index.html +83 -0
  3. package/dist/static/image/arrow.6f27b59f.png +0 -0
  4. package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
  5. package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
  6. package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
  7. package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
  8. package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
  9. package/dist/static/js/365.f05233ab.js +8462 -0
  10. package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
  11. package/dist/static/js/async/738.efa27644.js +1 -0
  12. package/dist/static/js/index.092ec5be.js +56 -0
  13. package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
  14. package/dist/static/js/lib-react.5c9129e0.js +2 -0
  15. package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
  16. package/package.json +104 -0
  17. package/src/assets/destroy_stage_0.png +0 -0
  18. package/src/assets/destroy_stage_1.png +0 -0
  19. package/src/assets/destroy_stage_2.png +0 -0
  20. package/src/assets/destroy_stage_3.png +0 -0
  21. package/src/assets/destroy_stage_4.png +0 -0
  22. package/src/assets/destroy_stage_5.png +0 -0
  23. package/src/assets/destroy_stage_6.png +0 -0
  24. package/src/assets/destroy_stage_7.png +0 -0
  25. package/src/assets/destroy_stage_8.png +0 -0
  26. package/src/assets/destroy_stage_9.png +0 -0
  27. package/src/examples/README.md +146 -0
  28. package/src/examples/appViewerExample.ts +205 -0
  29. package/src/examples/initialMenuStart.ts +161 -0
  30. package/src/graphicsBackend/appViewer.ts +297 -0
  31. package/src/graphicsBackend/config.ts +119 -0
  32. package/src/graphicsBackend/index.ts +10 -0
  33. package/src/graphicsBackend/playerState.ts +61 -0
  34. package/src/graphicsBackend/types.ts +143 -0
  35. package/src/index.ts +97 -0
  36. package/src/lib/DebugGui.ts +190 -0
  37. package/src/lib/animationController.ts +85 -0
  38. package/src/lib/buildSharedConfig.mjs +1 -0
  39. package/src/lib/cameraBobbing.ts +94 -0
  40. package/src/lib/canvas2DOverlay.example.ts +361 -0
  41. package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
  42. package/src/lib/canvas2DOverlay.ts +381 -0
  43. package/src/lib/cleanupDecorator.ts +29 -0
  44. package/src/lib/createPlayerObject.ts +55 -0
  45. package/src/lib/frameTimingCollector.ts +164 -0
  46. package/src/lib/guiRenderer.ts +283 -0
  47. package/src/lib/items.ts +140 -0
  48. package/src/lib/mesherlogReader.ts +131 -0
  49. package/src/lib/moreBlockDataGenerated.json +714 -0
  50. package/src/lib/preflatMap.json +1741 -0
  51. package/src/lib/simpleUtils.ts +40 -0
  52. package/src/lib/smoothSwitcher.ts +168 -0
  53. package/src/lib/spiral.ts +29 -0
  54. package/src/lib/ui/newStats.ts +120 -0
  55. package/src/lib/utils/proxy.ts +23 -0
  56. package/src/lib/utils/skins.ts +63 -0
  57. package/src/lib/utils.ts +76 -0
  58. package/src/lib/workerProxy.ts +342 -0
  59. package/src/lib/worldrendererCommon.ts +1088 -0
  60. package/src/mesher/mesher.ts +253 -0
  61. package/src/mesher/models.ts +769 -0
  62. package/src/mesher/modelsGeometryCommon.ts +142 -0
  63. package/src/mesher/shared.ts +80 -0
  64. package/src/mesher/standaloneRenderer.ts +270 -0
  65. package/src/mesher/test/a.ts +3 -0
  66. package/src/mesher/test/mesherTester.ts +76 -0
  67. package/src/mesher/test/playground.ts +19 -0
  68. package/src/mesher/test/test-perf.ts +74 -0
  69. package/src/mesher/test/tests.test.ts +56 -0
  70. package/src/mesher/world.ts +294 -0
  71. package/src/mesher/worldConstants.ts +1 -0
  72. package/src/modules/index.ts +11 -0
  73. package/src/modules/starfield.ts +313 -0
  74. package/src/modules/types.ts +110 -0
  75. package/src/playerState/playerState.ts +78 -0
  76. package/src/playerState/types.ts +36 -0
  77. package/src/playground/allEntitiesDebug.ts +170 -0
  78. package/src/playground/baseScene.ts +587 -0
  79. package/src/playground/mobileControls.tsx +268 -0
  80. package/src/playground/playground.html +83 -0
  81. package/src/playground/playground.ts +11 -0
  82. package/src/playground/playgroundUi.tsx +140 -0
  83. package/src/playground/reactUtils.ts +71 -0
  84. package/src/playground/scenes/allEntities.ts +13 -0
  85. package/src/playground/scenes/entities.ts +37 -0
  86. package/src/playground/scenes/floorRandom.ts +33 -0
  87. package/src/playground/scenes/frequentUpdates.ts +148 -0
  88. package/src/playground/scenes/geometryExport.ts +142 -0
  89. package/src/playground/scenes/index.ts +12 -0
  90. package/src/playground/scenes/lightingStarfield.ts +40 -0
  91. package/src/playground/scenes/main.ts +313 -0
  92. package/src/playground/scenes/railsCobweb.ts +14 -0
  93. package/src/playground/scenes/rotationIssue.ts +7 -0
  94. package/src/playground/scenes/slabsOptimization.ts +16 -0
  95. package/src/playground/scenes/transparencyIssue.ts +11 -0
  96. package/src/playground/shared.ts +79 -0
  97. package/src/resourcesManager/index.ts +5 -0
  98. package/src/resourcesManager/resourcesManager.ts +314 -0
  99. package/src/shims/minecraftData.ts +41 -0
  100. package/src/sign-renderer/index.html +21 -0
  101. package/src/sign-renderer/index.ts +216 -0
  102. package/src/sign-renderer/noop.js +1 -0
  103. package/src/sign-renderer/playground.ts +38 -0
  104. package/src/sign-renderer/tests.test.ts +69 -0
  105. package/src/sign-renderer/vite.config.ts +10 -0
  106. package/src/three/appShared.ts +75 -0
  107. package/src/three/bannerRenderer.ts +275 -0
  108. package/src/three/cameraShake.ts +120 -0
  109. package/src/three/cinimaticScript.ts +350 -0
  110. package/src/three/documentRenderer.ts +491 -0
  111. package/src/three/entities.ts +1580 -0
  112. package/src/three/entity/EntityMesh.ts +707 -0
  113. package/src/three/entity/animations.js +171 -0
  114. package/src/three/entity/armorModels.json +204 -0
  115. package/src/three/entity/armorModels.ts +36 -0
  116. package/src/three/entity/entities.json +6230 -0
  117. package/src/three/entity/exportedModels.js +38 -0
  118. package/src/three/entity/externalTextures.json +1 -0
  119. package/src/three/entity/models/allay.obj +325 -0
  120. package/src/three/entity/models/arrow.obj +60 -0
  121. package/src/three/entity/models/axolotl.obj +509 -0
  122. package/src/three/entity/models/blaze.obj +601 -0
  123. package/src/three/entity/models/boat.obj +417 -0
  124. package/src/three/entity/models/camel.obj +1061 -0
  125. package/src/three/entity/models/cat.obj +509 -0
  126. package/src/three/entity/models/chicken.obj +371 -0
  127. package/src/three/entity/models/cod.obj +371 -0
  128. package/src/three/entity/models/creeper.obj +279 -0
  129. package/src/three/entity/models/dolphin.obj +371 -0
  130. package/src/three/entity/models/ender_dragon.obj +2993 -0
  131. package/src/three/entity/models/enderman.obj +325 -0
  132. package/src/three/entity/models/endermite.obj +187 -0
  133. package/src/three/entity/models/fox.obj +463 -0
  134. package/src/three/entity/models/frog.obj +739 -0
  135. package/src/three/entity/models/ghast.obj +463 -0
  136. package/src/three/entity/models/goat.obj +601 -0
  137. package/src/three/entity/models/guardian.obj +1015 -0
  138. package/src/three/entity/models/horse.obj +1061 -0
  139. package/src/three/entity/models/llama.obj +509 -0
  140. package/src/three/entity/models/minecart.obj +233 -0
  141. package/src/three/entity/models/parrot.obj +509 -0
  142. package/src/three/entity/models/piglin.obj +739 -0
  143. package/src/three/entity/models/pillager.obj +371 -0
  144. package/src/three/entity/models/rabbit.obj +555 -0
  145. package/src/three/entity/models/sheep.obj +555 -0
  146. package/src/three/entity/models/shulker.obj +141 -0
  147. package/src/three/entity/models/sniffer.obj +693 -0
  148. package/src/three/entity/models/spider.obj +509 -0
  149. package/src/three/entity/models/tadpole.obj +95 -0
  150. package/src/three/entity/models/turtle.obj +371 -0
  151. package/src/three/entity/models/vex.obj +325 -0
  152. package/src/three/entity/models/villager.obj +509 -0
  153. package/src/three/entity/models/warden.obj +463 -0
  154. package/src/three/entity/models/witch.obj +647 -0
  155. package/src/three/entity/models/wolf.obj +509 -0
  156. package/src/three/entity/models/zombie_villager.obj +463 -0
  157. package/src/three/entity/objModels.js +1 -0
  158. package/src/three/fireworks.ts +661 -0
  159. package/src/three/fireworksRenderer.ts +434 -0
  160. package/src/three/globals.d.ts +7 -0
  161. package/src/three/graphicsBackend.ts +274 -0
  162. package/src/three/graphicsBackendOffThread.ts +107 -0
  163. package/src/three/hand.ts +89 -0
  164. package/src/three/holdingBlock.ts +926 -0
  165. package/src/three/index.ts +20 -0
  166. package/src/three/itemMesh.ts +427 -0
  167. package/src/three/modules.d.ts +14 -0
  168. package/src/three/panorama.ts +308 -0
  169. package/src/three/panoramaShared.ts +1 -0
  170. package/src/three/renderSlot.ts +82 -0
  171. package/src/three/skyboxRenderer.ts +406 -0
  172. package/src/three/starField.ts +13 -0
  173. package/src/three/threeJsMedia.ts +731 -0
  174. package/src/three/threeJsMethods.ts +15 -0
  175. package/src/three/threeJsParticles.ts +160 -0
  176. package/src/three/threeJsSound.ts +95 -0
  177. package/src/three/threeJsUtils.ts +90 -0
  178. package/src/three/waypointSprite.ts +435 -0
  179. package/src/three/waypoints.ts +163 -0
  180. package/src/three/world/cursorBlock.ts +172 -0
  181. package/src/three/world/vr.ts +257 -0
  182. package/src/three/worldGeometryExport.ts +259 -0
  183. package/src/three/worldGeometryHandler.ts +279 -0
  184. package/src/three/worldRendererThree.ts +1381 -0
  185. package/src/worldView/index.ts +6 -0
  186. package/src/worldView/types.ts +66 -0
  187. package/src/worldView/worldView.ts +424 -0
@@ -0,0 +1,172 @@
1
+ import * as THREE from 'three'
2
+ import { LineMaterial, LineSegmentsGeometry, Wireframe } from 'three-stdlib'
3
+ import { Vec3 } from 'vec3'
4
+ import { WorldRendererThree } from '../worldRendererThree'
5
+ import { loadThreeJsTextureFromUrl } from '../threeJsUtils'
6
+ import destroyStage0 from '../../assets/destroy_stage_0.png'
7
+ import destroyStage1 from '../../assets/destroy_stage_1.png'
8
+ import destroyStage2 from '../../assets/destroy_stage_2.png'
9
+ import destroyStage3 from '../../assets/destroy_stage_3.png'
10
+ import destroyStage4 from '../../assets/destroy_stage_4.png'
11
+ import destroyStage5 from '../../assets/destroy_stage_5.png'
12
+ import destroyStage6 from '../../assets/destroy_stage_6.png'
13
+ import destroyStage7 from '../../assets/destroy_stage_7.png'
14
+ import destroyStage8 from '../../assets/destroy_stage_8.png'
15
+ import destroyStage9 from '../../assets/destroy_stage_9.png'
16
+ import { BlockShape, BlocksShapes } from '@/playerState/types'
17
+
18
+ export class CursorBlock {
19
+ _cursorLinesHidden = false
20
+ get cursorLinesHidden() {
21
+ return this._cursorLinesHidden
22
+ }
23
+ set cursorLinesHidden(value: boolean) {
24
+ if (this.interactionLines) {
25
+ this.interactionLines.mesh.visible = !value
26
+ }
27
+ this._cursorLinesHidden = value
28
+ }
29
+
30
+ cursorLineMaterial!: LineMaterial
31
+ interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null
32
+ prevColor: string | undefined
33
+ blockBreakMesh: THREE.Mesh
34
+ breakTextures: THREE.Texture[] = []
35
+
36
+ constructor(public readonly worldRenderer: WorldRendererThree) {
37
+ // Initialize break mesh and textures
38
+ const destroyStagesImages = [
39
+ destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4,
40
+ destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9
41
+ ]
42
+
43
+ for (let i = 0; i < 10; i++) {
44
+ void loadThreeJsTextureFromUrl(destroyStagesImages[i]).then((texture) => {
45
+ texture.magFilter = THREE.NearestFilter
46
+ texture.minFilter = THREE.NearestFilter
47
+ this.breakTextures.push(texture)
48
+ })
49
+ }
50
+
51
+ const breakMaterial = new THREE.MeshBasicMaterial({
52
+ transparent: true,
53
+ blending: THREE.MultiplyBlending,
54
+ alphaTest: 0.5,
55
+ })
56
+ this.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial)
57
+ this.blockBreakMesh.visible = false
58
+ this.blockBreakMesh.renderOrder = 999
59
+ this.blockBreakMesh.name = 'blockBreakMesh'
60
+ this.worldRenderer.scene.add(this.blockBreakMesh)
61
+
62
+ this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
63
+ this.updateLineMaterial()
64
+ })
65
+ // todo figure out why otherwise fog from skybox breaks it
66
+ setTimeout(() => {
67
+ this.updateLineMaterial()
68
+ if (this.interactionLines) {
69
+ this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true)
70
+ }
71
+ })
72
+ }
73
+
74
+ // Update functions
75
+ updateLineMaterial() {
76
+ const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative'
77
+ const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
78
+
79
+ if (this.cursorLineMaterial) {
80
+ this.cursorLineMaterial.dispose()
81
+ }
82
+ this.cursorLineMaterial = new LineMaterial({
83
+ color: (() => {
84
+ switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
85
+ case 'blue':
86
+ return 0x40_80_ff
87
+ case 'classic':
88
+ return 0x00_00_00
89
+ default:
90
+ return inCreative ? 0x40_80_ff : 0x00_00_00
91
+ }
92
+ })(),
93
+ linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
94
+ // dashed: true,
95
+ // dashSize: 5,
96
+ })
97
+ this.prevColor = this.worldRenderer.worldRendererConfig.highlightBlockColor
98
+ }
99
+
100
+ updateBreakAnimation(blockPosition: { x: number, y: number, z: number } | undefined, stage: number | null, mergedShape?: BlockShape) {
101
+ this.hideBreakAnimation()
102
+ if (stage === null || !blockPosition || !mergedShape) return
103
+
104
+ const { position: _position, width, height, depth } = mergedShape
105
+ const position = new Vec3(_position.x, _position.y, _position.z)
106
+ this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001)
107
+ position.add(new Vec3(blockPosition.x, blockPosition.y, blockPosition.z))
108
+ this.blockBreakMesh.position.set(position.x, position.y, position.z)
109
+ this.blockBreakMesh.visible = true;
110
+
111
+ (this.blockBreakMesh.material as THREE.MeshBasicMaterial).map = this.breakTextures[stage] ?? this.breakTextures.at(-1);
112
+ (this.blockBreakMesh.material as THREE.MeshBasicMaterial).needsUpdate = true
113
+ }
114
+
115
+ hideBreakAnimation() {
116
+ if (this.blockBreakMesh) {
117
+ this.blockBreakMesh.visible = false
118
+ }
119
+ }
120
+
121
+ updateDisplay() {
122
+ if (this.cursorLineMaterial) {
123
+ const { renderer } = this.worldRenderer
124
+ this.cursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
125
+ this.cursorLineMaterial.dashOffset = performance.now() / 750
126
+ }
127
+ }
128
+
129
+ setHighlightCursorBlock(blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void {
130
+ if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && sameArray(shapePositions ?? [], this.interactionLines.shapePositions ?? []) && !force) {
131
+ return
132
+ }
133
+ if (this.interactionLines !== null) {
134
+ this.worldRenderer.scene.remove(this.interactionLines.mesh)
135
+ this.interactionLines = null
136
+ }
137
+ if (blockPos === null) {
138
+ return
139
+ }
140
+
141
+ const group = new THREE.Group()
142
+ for (const { position: _position, width, height, depth } of shapePositions ?? []) {
143
+ const position = new Vec3(_position.x, _position.y, _position.z)
144
+ const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const
145
+ const geometry = new THREE.BoxGeometry(...scale)
146
+ const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
147
+ const wireframe = new Wireframe(lines, this.cursorLineMaterial)
148
+ const pos = blockPos.plus(position)
149
+ wireframe.position.set(pos.x, pos.y, pos.z)
150
+ wireframe.computeLineDistances()
151
+ group.add(wireframe)
152
+ }
153
+ this.worldRenderer.scene.add(group)
154
+ group.visible = !this.cursorLinesHidden
155
+ this.interactionLines = { blockPos, mesh: group, shapePositions }
156
+ }
157
+
158
+ render() {
159
+ if (this.prevColor !== this.worldRenderer.worldRendererConfig.highlightBlockColor) {
160
+ this.updateLineMaterial()
161
+ }
162
+ this.updateDisplay()
163
+ }
164
+ }
165
+
166
+ const sameArray = (a: any[], b: any[]) => {
167
+ if (a.length !== b.length) return false
168
+ for (const [i, element] of a.entries()) {
169
+ if (element !== b[i]) return false
170
+ }
171
+ return true
172
+ }
@@ -0,0 +1,257 @@
1
+ import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'
2
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
3
+ import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js'
4
+ import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad'
5
+ import * as THREE from 'three'
6
+ import { WorldRendererThree } from '../worldRendererThree'
7
+ import { DocumentRenderer } from '../documentRenderer'
8
+
9
+ export async function initVR(worldRenderer: WorldRendererThree, documentRenderer: DocumentRenderer) {
10
+ if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return
11
+ const { renderer } = worldRenderer
12
+
13
+ const isSupported = await checkVRSupport()
14
+ if (!isSupported) return
15
+
16
+ enableVr()
17
+
18
+ const vrButtonContainer = createVrButtonContainer(renderer)
19
+ const updateVrButtons = () => {
20
+ const newHidden = !worldRenderer.worldRendererConfig.vrSupport || !worldRenderer.worldRendererConfig.foreground
21
+ if (vrButtonContainer.hidden !== newHidden) {
22
+ vrButtonContainer.hidden = newHidden
23
+ }
24
+ }
25
+
26
+ worldRenderer.onRender.push(updateVrButtons)
27
+
28
+ function enableVr() {
29
+ renderer.xr.enabled = true
30
+ // renderer.xr.setReferenceSpaceType('local-floor')
31
+ worldRenderer.reactiveState.preventEscapeMenu = true
32
+ }
33
+
34
+ function disableVr() {
35
+ renderer.xr.enabled = false
36
+ worldRenderer.cameraGroupVr = undefined
37
+ worldRenderer.reactiveState.preventEscapeMenu = false
38
+ worldRenderer.scene.remove(user)
39
+ vrButtonContainer.hidden = true
40
+ }
41
+
42
+ function createVrButtonContainer(renderer) {
43
+ const container = document.createElement('div')
44
+ const vrButton = VRButton.createButton(renderer)
45
+ styleContainer(container)
46
+
47
+ const closeButton = createCloseButton(container)
48
+
49
+ container.appendChild(vrButton)
50
+ container.appendChild(closeButton)
51
+ document.body.appendChild(container)
52
+
53
+ return container
54
+ }
55
+
56
+ function styleContainer(container: HTMLElement) {
57
+ typedAssign(container.style, {
58
+ position: 'absolute',
59
+ bottom: '80px',
60
+ left: '0',
61
+ right: '0',
62
+ display: 'flex',
63
+ justifyContent: 'center',
64
+ zIndex: '8',
65
+ gap: '8px',
66
+ })
67
+ }
68
+
69
+ function createCloseButton(container: HTMLElement) {
70
+ const closeButton = document.createElement('button')
71
+ closeButton.textContent = 'X'
72
+ typedAssign(closeButton.style, {
73
+ padding: '0 12px',
74
+ color: 'white',
75
+ fontSize: '14px',
76
+ lineHeight: '20px',
77
+ cursor: 'pointer',
78
+ background: 'transparent',
79
+ border: '1px solid rgb(255, 255, 255)',
80
+ borderRadius: '4px',
81
+ opacity: '0.7',
82
+ })
83
+
84
+ closeButton.addEventListener('click', () => {
85
+ container.hidden = true
86
+ worldRenderer.worldRendererConfig.vrSupport = false
87
+ })
88
+
89
+ return closeButton
90
+ }
91
+
92
+
93
+ async function checkVRSupport() {
94
+ try {
95
+ const supported = await navigator.xr?.isSessionSupported('immersive-vr')
96
+ return supported && !!XRSession.prototype.updateRenderState
97
+ } catch (err) {
98
+ console.error('Error checking if VR is supported', err)
99
+ return false
100
+ }
101
+ }
102
+
103
+ // hack for vr camera
104
+ const user = new THREE.Group()
105
+ user.name = 'vr-camera-container'
106
+ worldRenderer.scene.add(user)
107
+ const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader())
108
+ const controller1 = renderer.xr.getControllerGrip(0)
109
+ const controller2 = renderer.xr.getControllerGrip(1)
110
+
111
+ // todo the logic written here can be hard to understand as it was designed to work in gamepad api emulation mode, will be refactored once there is a contro-max rewrite is done
112
+ const virtualGamepadIndex = 4
113
+ let connectedVirtualGamepad
114
+ //@ts-expect-error
115
+ const manageXrInputSource = ({ gamepad, handedness = defaultHandedness }, defaultHandedness, removeAction = false) => {
116
+ if (handedness === 'right') {
117
+ const event: any = new Event(removeAction ? 'gamepaddisconnected' : 'gamepadconnected') // todo need to expose and use external gamepads api in contro-max instead
118
+ event.gamepad = removeAction ? connectedVirtualGamepad : { ...gamepad, mapping: 'standard', index: virtualGamepadIndex }
119
+ connectedVirtualGamepad = event.gamepad
120
+ window.dispatchEvent(event)
121
+ }
122
+ }
123
+ let hand1: any = controllerModelFactory.createControllerModel(controller1)
124
+ controller1.addEventListener('connected', (event) => {
125
+ hand1.xrInputSource = event.data
126
+ manageXrInputSource(event.data, 'left')
127
+ user.add(controller1)
128
+ })
129
+ controller1.add(hand1)
130
+ let hand2: any = controllerModelFactory.createControllerModel(controller2)
131
+ controller2.addEventListener('connected', (event) => {
132
+ hand2.xrInputSource = event.data
133
+ manageXrInputSource(event.data, 'right')
134
+ user.add(controller2)
135
+ })
136
+ controller2.add(hand2)
137
+
138
+ controller1.addEventListener('disconnected', () => {
139
+ // don't handle removal of gamepads for now as is don't affect contro-max
140
+ manageXrInputSource(hand1.xrInputSource, 'left', true)
141
+ hand1.xrInputSource = undefined
142
+ })
143
+ controller2.addEventListener('disconnected', () => {
144
+ manageXrInputSource(hand1.xrInputSource, 'right', true)
145
+ hand2.xrInputSource = undefined
146
+ })
147
+
148
+ const originalGetGamepads = navigator.getGamepads.bind(navigator)
149
+ // is it okay to patch this?
150
+ //@ts-expect-error
151
+ navigator.getGamepads = () => {
152
+ const originalGamepads = originalGetGamepads()
153
+ if (!hand1.xrInputSource || !hand2.xrInputSource) return originalGamepads
154
+ return [
155
+ ...originalGamepads,
156
+ {
157
+ axes: remapAxes(hand2.xrInputSource.gamepad.axes, hand1.xrInputSource.gamepad.axes),
158
+ buttons: remapButtons(hand2.xrInputSource.gamepad.buttons, hand1.xrInputSource.gamepad.buttons),
159
+ connected: true,
160
+ mapping: 'standard',
161
+ id: '',
162
+ index: virtualGamepadIndex
163
+ }
164
+ ]
165
+ }
166
+
167
+ let rotSnapReset = true
168
+ let yawOffset = 0
169
+ renderer.setAnimationLoop(() => {
170
+ if (!renderer.xr.isPresenting) return
171
+ if (hand1.xrInputSource && hand2.xrInputSource) {
172
+ hand1.xAxis = hand1.xrInputSource.gamepad.axes[2]
173
+ hand1.yAxis = hand1.xrInputSource.gamepad.axes[3]
174
+ hand2.xAxis = hand2.xrInputSource.gamepad.axes[2]
175
+ hand2.yAxis = hand2.xrInputSource.gamepad.axes[3]
176
+ // hand2 should be right
177
+ if (hand1.xrInputSource.handedness === 'right') {
178
+ const tmp = hand2
179
+ hand2 = hand1
180
+ hand1 = tmp
181
+ }
182
+ }
183
+
184
+ if (rotSnapReset) {
185
+ if (Math.abs(hand1.xAxis) > 0.8) {
186
+ yawOffset -= Math.PI / 4 * Math.sign(hand1.xAxis)
187
+ rotSnapReset = false
188
+ }
189
+ } else if (Math.abs(hand1.xAxis) < 0.1) {
190
+ rotSnapReset = true
191
+ }
192
+
193
+ // appViewer.backend?.updateCamera(null, yawOffset, 0)
194
+ // worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch)
195
+
196
+ // todo restore this logic (need to preserve ability to move camera)
197
+ // const xrCamera = renderer.xr.getCamera()
198
+ // const d = xrCamera.getWorldDirection(new THREE.Vector3())
199
+ // bot.entity.yaw = Math.atan2(-d.x, -d.z)
200
+ // bot.entity.pitch = Math.asin(d.y)
201
+
202
+ documentRenderer.frameRender(false)
203
+ })
204
+ renderer.xr.addEventListener('sessionstart', () => {
205
+ user.add(worldRenderer.camera)
206
+ worldRenderer.cameraGroupVr = user
207
+ })
208
+ renderer.xr.addEventListener('sessionend', () => {
209
+ worldRenderer.cameraGroupVr = undefined
210
+ user.remove(worldRenderer.camera)
211
+ })
212
+
213
+ worldRenderer.abortController.signal.addEventListener('abort', disableVr)
214
+ }
215
+
216
+ const xrStandardRightButtonsMap = [
217
+ [0 /* trigger */, 'Right Trigger'],
218
+ [1 /* squeeze */, 'Right Bumper'],
219
+ // need to think of a way to support touchpad input
220
+ [3 /* Thumbstick Press */, 'Right Stick'],
221
+ [4 /* A */, 'A'],
222
+ [5 /* B */, 'B'],
223
+ ]
224
+ const xrStandardLeftButtonsMap = [
225
+ [0 /* trigger */, 'Left Trigger'],
226
+ [1 /* squeeze */, 'Left Bumper'],
227
+ // need to think of a way to support touchpad input
228
+ [3 /* Thumbstick Press */, 'Left Stick'],
229
+ [4 /* A */, 'X'],
230
+ [5 /* B */, 'Y'],
231
+ ]
232
+ const remapButtons = (rightButtons: any[], leftButtons: any[]) => {
233
+ // return remapped buttons
234
+ const remapped = [] as string[]
235
+ const remapWithMap = (buttons, map) => {
236
+ for (const [index, standardName] of map) {
237
+ const standardMappingIndex = standardButtonsMap.findIndex((aliases) => aliases.find(alias => standardName === alias))
238
+ remapped[standardMappingIndex] = buttons[index]
239
+ }
240
+ }
241
+ remapWithMap(rightButtons, xrStandardRightButtonsMap)
242
+ remapWithMap(leftButtons, xrStandardLeftButtonsMap)
243
+ return remapped
244
+ }
245
+ const remapAxes = (axesRight, axesLeft) => {
246
+ // 0, 1 are reserved for touch
247
+ return [
248
+ axesLeft[2],
249
+ axesLeft[3],
250
+ axesRight[2],
251
+ axesRight[3]
252
+ ]
253
+ }
254
+
255
+ function typedAssign<T extends Record<string, any>>(target: T, source: Partial<T>) {
256
+ Object.assign(target, source)
257
+ }
@@ -0,0 +1,259 @@
1
+ import * as THREE from 'three'
2
+ import type { WorldRendererThree } from './worldRendererThree'
3
+
4
+ const GEOMETRY_EXPORT_GROUP_NAME = 'geometry-export-root'
5
+
6
+ // Format for exported world geometry
7
+ export interface ExportedWorldGeometry {
8
+ version: string
9
+ exportedAt: string
10
+ camera: {
11
+ position: { x: number, y: number, z: number }
12
+ rotation: { pitch: number, yaw: number }
13
+ }
14
+ sections: ExportedSection[]
15
+ textureAtlasDataUrl?: string
16
+ }
17
+
18
+ export interface ExportedSection {
19
+ key: string
20
+ position: { x: number, y: number, z: number }
21
+ geometry: {
22
+ positions: number[]
23
+ normals: number[]
24
+ colors: number[]
25
+ uvs: number[]
26
+ indices: number[]
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Export world geometry to a downloadable file
32
+ */
33
+ export function exportWorldGeometry(
34
+ worldRenderer: WorldRendererThree,
35
+ cameraPosition: { x: number, y: number, z: number },
36
+ cameraRotation: { pitch: number, yaw: number },
37
+ includeTexture = false
38
+ ): ExportedWorldGeometry {
39
+ const sections: ExportedSection[] = []
40
+
41
+ for (const [key, sectionObject] of Object.entries(worldRenderer.sectionObjects)) {
42
+ const mesh = sectionObject.children.find(child => child.name === 'mesh') as THREE.Mesh | undefined
43
+ if (!mesh?.geometry) continue
44
+
45
+ const { geometry } = mesh
46
+ const positionAttr = geometry.getAttribute('position') as THREE.BufferAttribute
47
+ const normalAttr = geometry.getAttribute('normal') as THREE.BufferAttribute
48
+ const colorAttr = geometry.getAttribute('color') as THREE.BufferAttribute
49
+ const uvAttr = geometry.getAttribute('uv') as THREE.BufferAttribute
50
+ const indexAttr = geometry.index!
51
+
52
+ if (!positionAttr || !indexAttr) continue
53
+
54
+ sections.push({
55
+ key,
56
+ position: {
57
+ x: mesh.position.x,
58
+ y: mesh.position.y,
59
+ z: mesh.position.z
60
+ },
61
+ geometry: {
62
+ positions: [...positionAttr.array],
63
+ normals: normalAttr ? [...normalAttr.array] : [],
64
+ colors: colorAttr ? [...colorAttr.array] : [],
65
+ uvs: uvAttr ? [...uvAttr.array] : [],
66
+ indices: [...indexAttr.array]
67
+ }
68
+ })
69
+ }
70
+
71
+ const exportData: ExportedWorldGeometry = {
72
+ version: worldRenderer.version ?? 'unknown',
73
+ exportedAt: new Date().toISOString(),
74
+ camera: {
75
+ position: cameraPosition,
76
+ rotation: cameraRotation
77
+ },
78
+ sections
79
+ }
80
+
81
+ // Optionally include texture atlas as data URL
82
+ if (includeTexture && worldRenderer.material.map) {
83
+ const canvas = document.createElement('canvas')
84
+ const texture = worldRenderer.material.map
85
+ const { image } = texture
86
+ if (image) {
87
+ canvas.width = image.width
88
+ canvas.height = image.height
89
+ const ctx = canvas.getContext('2d')!
90
+ ctx.drawImage(image, 0, 0)
91
+ exportData.textureAtlasDataUrl = canvas.toDataURL('image/png')
92
+ }
93
+ }
94
+
95
+ return exportData
96
+ }
97
+
98
+ /**
99
+ * Download world geometry as JSON file
100
+ */
101
+ export function downloadWorldGeometry(
102
+ worldRenderer: WorldRendererThree,
103
+ cameraPosition: { x: number, y: number, z: number },
104
+ cameraRotation: { pitch: number, yaw: number },
105
+ filename = 'world-geometry.json',
106
+ includeTexture = false
107
+ ) {
108
+ const exportData = exportWorldGeometry(worldRenderer, cameraPosition, cameraRotation, includeTexture)
109
+ const json = JSON.stringify(exportData)
110
+ const blob = new Blob([json], { type: 'application/json' })
111
+ const url = URL.createObjectURL(blob)
112
+
113
+ const a = document.createElement('a')
114
+ a.href = url
115
+ a.download = filename
116
+ a.click()
117
+
118
+ URL.revokeObjectURL(url)
119
+ }
120
+
121
+ /**
122
+ * Load world geometry from URL
123
+ */
124
+ export async function loadWorldGeometryFromUrl(url: string): Promise<ExportedWorldGeometry> {
125
+ const response = await fetch(url)
126
+ if (!response.ok) {
127
+ throw new Error(`Failed to fetch world geometry: ${response.statusText}`)
128
+ }
129
+ return response.json()
130
+ }
131
+
132
+ /**
133
+ * Recreate THREE.js meshes from exported geometry
134
+ * Returns an array of mesh groups that can be added to a scene
135
+ */
136
+ export function createMeshesFromExport(
137
+ exportData: ExportedWorldGeometry,
138
+ material: THREE.Material
139
+ ): THREE.Group[] {
140
+ const groups: THREE.Group[] = []
141
+
142
+ for (const section of exportData.sections) {
143
+ const geometry = new THREE.BufferGeometry()
144
+
145
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(section.geometry.positions, 3))
146
+ if (section.geometry.normals.length) {
147
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(section.geometry.normals, 3))
148
+ }
149
+ if (section.geometry.colors.length) {
150
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(section.geometry.colors, 3))
151
+ }
152
+ if (section.geometry.uvs.length) {
153
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(section.geometry.uvs, 2))
154
+ }
155
+
156
+ // Use appropriate index type based on vertex count
157
+ const maxIndex = Math.max(...section.geometry.indices)
158
+ const IndexArrayType = maxIndex > 65_535 ? Uint32Array : Uint16Array
159
+ geometry.setIndex(new THREE.BufferAttribute(new IndexArrayType(section.geometry.indices), 1))
160
+
161
+ const mesh = new THREE.Mesh(geometry, material)
162
+ mesh.position.set(section.position.x, section.position.y, section.position.z)
163
+ mesh.name = 'mesh'
164
+
165
+ const group = new THREE.Group()
166
+ group.name = 'chunk'
167
+ group.add(mesh)
168
+
169
+ groups.push(group)
170
+ }
171
+
172
+ return groups
173
+ }
174
+
175
+ /**
176
+ * Load texture from data URL and create THREE.js texture
177
+ */
178
+ export async function loadTextureFromDataUrl(dataUrl: string): Promise<THREE.Texture> {
179
+ return new Promise((resolve, reject) => {
180
+ const image = new Image()
181
+ image.onload = () => {
182
+ const texture = new THREE.Texture(image)
183
+ texture.magFilter = THREE.NearestFilter
184
+ texture.minFilter = THREE.NearestFilter
185
+ texture.needsUpdate = true
186
+ texture.flipY = false
187
+ resolve(texture)
188
+ }
189
+ image.onerror = reject
190
+ image.src = dataUrl
191
+ })
192
+ }
193
+
194
+ const disposeGeometryExportGroup = (group: THREE.Object3D) => {
195
+ group.traverse(obj => {
196
+ if ((obj as THREE.Mesh).isMesh) {
197
+ const mesh = obj as THREE.Mesh
198
+ mesh.geometry?.dispose()
199
+ }
200
+ })
201
+
202
+ const material = group.userData?.geometryExportMaterial as THREE.Material | undefined
203
+ if (material) {
204
+ const texture = (material as THREE.MeshLambertMaterial).map
205
+ texture?.dispose?.()
206
+ material.dispose?.()
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Apply exported geometry to an existing WorldRenderer instance.
212
+ * Replaces any previously imported geometry export group.
213
+ */
214
+ export async function applyWorldGeometryExport(
215
+ worldRenderer: WorldRendererThree,
216
+ exportData: ExportedWorldGeometry
217
+ ): Promise<number> {
218
+ const {
219
+ scene,
220
+ renderUpdateEmitter,
221
+ material: rendererMaterial
222
+ } = worldRenderer
223
+ const existingGroup = scene.getObjectByName(GEOMETRY_EXPORT_GROUP_NAME)
224
+ if (existingGroup) {
225
+ scene.remove(existingGroup)
226
+ disposeGeometryExportGroup(existingGroup)
227
+ }
228
+
229
+ const hasEmbeddedTexture = !!exportData.textureAtlasDataUrl
230
+ let material: THREE.Material
231
+ if (hasEmbeddedTexture && exportData.textureAtlasDataUrl) {
232
+ const texture = await loadTextureFromDataUrl(exportData.textureAtlasDataUrl)
233
+ material = new THREE.MeshLambertMaterial({
234
+ map: texture,
235
+ vertexColors: true,
236
+ transparent: true,
237
+ alphaTest: 0.1
238
+ })
239
+ material.name = 'geometry-export-material'
240
+ } else {
241
+ material = rendererMaterial
242
+ }
243
+
244
+ const groups = createMeshesFromExport(exportData, material)
245
+ const container = new THREE.Group()
246
+ container.name = GEOMETRY_EXPORT_GROUP_NAME
247
+ if (hasEmbeddedTexture) {
248
+ container.userData.geometryExportMaterial = material
249
+ }
250
+
251
+ for (const group of groups) {
252
+ container.add(group)
253
+ }
254
+
255
+ scene.add(container)
256
+ renderUpdateEmitter.emit('update')
257
+
258
+ return groups.length
259
+ }