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.
- package/README.md +297 -0
- package/dist/index.html +83 -0
- package/dist/static/image/arrow.6f27b59f.png +0 -0
- package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
- package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
- package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
- package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
- package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
- package/dist/static/js/365.f05233ab.js +8462 -0
- package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
- package/dist/static/js/async/738.efa27644.js +1 -0
- package/dist/static/js/index.092ec5be.js +56 -0
- package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
- package/dist/static/js/lib-react.5c9129e0.js +2 -0
- package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
- package/package.json +104 -0
- package/src/assets/destroy_stage_0.png +0 -0
- package/src/assets/destroy_stage_1.png +0 -0
- package/src/assets/destroy_stage_2.png +0 -0
- package/src/assets/destroy_stage_3.png +0 -0
- package/src/assets/destroy_stage_4.png +0 -0
- package/src/assets/destroy_stage_5.png +0 -0
- package/src/assets/destroy_stage_6.png +0 -0
- package/src/assets/destroy_stage_7.png +0 -0
- package/src/assets/destroy_stage_8.png +0 -0
- package/src/assets/destroy_stage_9.png +0 -0
- package/src/examples/README.md +146 -0
- package/src/examples/appViewerExample.ts +205 -0
- package/src/examples/initialMenuStart.ts +161 -0
- package/src/graphicsBackend/appViewer.ts +297 -0
- package/src/graphicsBackend/config.ts +119 -0
- package/src/graphicsBackend/index.ts +10 -0
- package/src/graphicsBackend/playerState.ts +61 -0
- package/src/graphicsBackend/types.ts +143 -0
- package/src/index.ts +97 -0
- package/src/lib/DebugGui.ts +190 -0
- package/src/lib/animationController.ts +85 -0
- package/src/lib/buildSharedConfig.mjs +1 -0
- package/src/lib/cameraBobbing.ts +94 -0
- package/src/lib/canvas2DOverlay.example.ts +361 -0
- package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
- package/src/lib/canvas2DOverlay.ts +381 -0
- package/src/lib/cleanupDecorator.ts +29 -0
- package/src/lib/createPlayerObject.ts +55 -0
- package/src/lib/frameTimingCollector.ts +164 -0
- package/src/lib/guiRenderer.ts +283 -0
- package/src/lib/items.ts +140 -0
- package/src/lib/mesherlogReader.ts +131 -0
- package/src/lib/moreBlockDataGenerated.json +714 -0
- package/src/lib/preflatMap.json +1741 -0
- package/src/lib/simpleUtils.ts +40 -0
- package/src/lib/smoothSwitcher.ts +168 -0
- package/src/lib/spiral.ts +29 -0
- package/src/lib/ui/newStats.ts +120 -0
- package/src/lib/utils/proxy.ts +23 -0
- package/src/lib/utils/skins.ts +63 -0
- package/src/lib/utils.ts +76 -0
- package/src/lib/workerProxy.ts +342 -0
- package/src/lib/worldrendererCommon.ts +1088 -0
- package/src/mesher/mesher.ts +253 -0
- package/src/mesher/models.ts +769 -0
- package/src/mesher/modelsGeometryCommon.ts +142 -0
- package/src/mesher/shared.ts +80 -0
- package/src/mesher/standaloneRenderer.ts +270 -0
- package/src/mesher/test/a.ts +3 -0
- package/src/mesher/test/mesherTester.ts +76 -0
- package/src/mesher/test/playground.ts +19 -0
- package/src/mesher/test/test-perf.ts +74 -0
- package/src/mesher/test/tests.test.ts +56 -0
- package/src/mesher/world.ts +294 -0
- package/src/mesher/worldConstants.ts +1 -0
- package/src/modules/index.ts +11 -0
- package/src/modules/starfield.ts +313 -0
- package/src/modules/types.ts +110 -0
- package/src/playerState/playerState.ts +78 -0
- package/src/playerState/types.ts +36 -0
- package/src/playground/allEntitiesDebug.ts +170 -0
- package/src/playground/baseScene.ts +587 -0
- package/src/playground/mobileControls.tsx +268 -0
- package/src/playground/playground.html +83 -0
- package/src/playground/playground.ts +11 -0
- package/src/playground/playgroundUi.tsx +140 -0
- package/src/playground/reactUtils.ts +71 -0
- package/src/playground/scenes/allEntities.ts +13 -0
- package/src/playground/scenes/entities.ts +37 -0
- package/src/playground/scenes/floorRandom.ts +33 -0
- package/src/playground/scenes/frequentUpdates.ts +148 -0
- package/src/playground/scenes/geometryExport.ts +142 -0
- package/src/playground/scenes/index.ts +12 -0
- package/src/playground/scenes/lightingStarfield.ts +40 -0
- package/src/playground/scenes/main.ts +313 -0
- package/src/playground/scenes/railsCobweb.ts +14 -0
- package/src/playground/scenes/rotationIssue.ts +7 -0
- package/src/playground/scenes/slabsOptimization.ts +16 -0
- package/src/playground/scenes/transparencyIssue.ts +11 -0
- package/src/playground/shared.ts +79 -0
- package/src/resourcesManager/index.ts +5 -0
- package/src/resourcesManager/resourcesManager.ts +314 -0
- package/src/shims/minecraftData.ts +41 -0
- package/src/sign-renderer/index.html +21 -0
- package/src/sign-renderer/index.ts +216 -0
- package/src/sign-renderer/noop.js +1 -0
- package/src/sign-renderer/playground.ts +38 -0
- package/src/sign-renderer/tests.test.ts +69 -0
- package/src/sign-renderer/vite.config.ts +10 -0
- package/src/three/appShared.ts +75 -0
- package/src/three/bannerRenderer.ts +275 -0
- package/src/three/cameraShake.ts +120 -0
- package/src/three/cinimaticScript.ts +350 -0
- package/src/three/documentRenderer.ts +491 -0
- package/src/three/entities.ts +1580 -0
- package/src/three/entity/EntityMesh.ts +707 -0
- package/src/three/entity/animations.js +171 -0
- package/src/three/entity/armorModels.json +204 -0
- package/src/three/entity/armorModels.ts +36 -0
- package/src/three/entity/entities.json +6230 -0
- package/src/three/entity/exportedModels.js +38 -0
- package/src/three/entity/externalTextures.json +1 -0
- package/src/three/entity/models/allay.obj +325 -0
- package/src/three/entity/models/arrow.obj +60 -0
- package/src/three/entity/models/axolotl.obj +509 -0
- package/src/three/entity/models/blaze.obj +601 -0
- package/src/three/entity/models/boat.obj +417 -0
- package/src/three/entity/models/camel.obj +1061 -0
- package/src/three/entity/models/cat.obj +509 -0
- package/src/three/entity/models/chicken.obj +371 -0
- package/src/three/entity/models/cod.obj +371 -0
- package/src/three/entity/models/creeper.obj +279 -0
- package/src/three/entity/models/dolphin.obj +371 -0
- package/src/three/entity/models/ender_dragon.obj +2993 -0
- package/src/three/entity/models/enderman.obj +325 -0
- package/src/three/entity/models/endermite.obj +187 -0
- package/src/three/entity/models/fox.obj +463 -0
- package/src/three/entity/models/frog.obj +739 -0
- package/src/three/entity/models/ghast.obj +463 -0
- package/src/three/entity/models/goat.obj +601 -0
- package/src/three/entity/models/guardian.obj +1015 -0
- package/src/three/entity/models/horse.obj +1061 -0
- package/src/three/entity/models/llama.obj +509 -0
- package/src/three/entity/models/minecart.obj +233 -0
- package/src/three/entity/models/parrot.obj +509 -0
- package/src/three/entity/models/piglin.obj +739 -0
- package/src/three/entity/models/pillager.obj +371 -0
- package/src/three/entity/models/rabbit.obj +555 -0
- package/src/three/entity/models/sheep.obj +555 -0
- package/src/three/entity/models/shulker.obj +141 -0
- package/src/three/entity/models/sniffer.obj +693 -0
- package/src/three/entity/models/spider.obj +509 -0
- package/src/three/entity/models/tadpole.obj +95 -0
- package/src/three/entity/models/turtle.obj +371 -0
- package/src/three/entity/models/vex.obj +325 -0
- package/src/three/entity/models/villager.obj +509 -0
- package/src/three/entity/models/warden.obj +463 -0
- package/src/three/entity/models/witch.obj +647 -0
- package/src/three/entity/models/wolf.obj +509 -0
- package/src/three/entity/models/zombie_villager.obj +463 -0
- package/src/three/entity/objModels.js +1 -0
- package/src/three/fireworks.ts +661 -0
- package/src/three/fireworksRenderer.ts +434 -0
- package/src/three/globals.d.ts +7 -0
- package/src/three/graphicsBackend.ts +274 -0
- package/src/three/graphicsBackendOffThread.ts +107 -0
- package/src/three/hand.ts +89 -0
- package/src/three/holdingBlock.ts +926 -0
- package/src/three/index.ts +20 -0
- package/src/three/itemMesh.ts +427 -0
- package/src/three/modules.d.ts +14 -0
- package/src/three/panorama.ts +308 -0
- package/src/three/panoramaShared.ts +1 -0
- package/src/three/renderSlot.ts +82 -0
- package/src/three/skyboxRenderer.ts +406 -0
- package/src/three/starField.ts +13 -0
- package/src/three/threeJsMedia.ts +731 -0
- package/src/three/threeJsMethods.ts +15 -0
- package/src/three/threeJsParticles.ts +160 -0
- package/src/three/threeJsSound.ts +95 -0
- package/src/three/threeJsUtils.ts +90 -0
- package/src/three/waypointSprite.ts +435 -0
- package/src/three/waypoints.ts +163 -0
- package/src/three/world/cursorBlock.ts +172 -0
- package/src/three/world/vr.ts +257 -0
- package/src/three/worldGeometryExport.ts +259 -0
- package/src/three/worldGeometryHandler.ts +279 -0
- package/src/three/worldRendererThree.ts +1381 -0
- package/src/worldView/index.ts +6 -0
- package/src/worldView/types.ts +66 -0
- 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
|
+
}
|