Skip to content

Commit 5cab51b

Browse files
author
Зверев Александр
committed
refactor: render player model directly inside inventory component
1 parent 473d672 commit 5cab51b

2 files changed

Lines changed: 186 additions & 40 deletions

File tree

src/react/inventory/Inventory.tsx

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createPortal } from 'react-dom'
2-
import { useEffect, useMemo, useCallback, useRef, useState } from 'react'
2+
import { useEffect, useMemo, useCallback, useState } from 'react'
33
import { useSnapshot } from 'valtio'
44
import {
55
TextureProvider,
@@ -15,48 +15,11 @@ import { useAppScale } from '../../scaleInterface'
1515
import { activeModalStack, hideCurrentModal } from '../../globalState'
1616
import { options } from '../../optionsStorage'
1717
import { getJeiItems, getItemRecipes, getItemUsages } from '../../inventoryWindows'
18-
import { modelViewerState } from '../OverlayModelViewer'
18+
import { PlayerModelViewer } from './PlayerModelViewer'
1919
import { buildItemMapper, textureConfig, clearInventoryCaches, formatWindowTitle } from './sharedConnectorSetup'
2020

2121
export { clearInventoryCaches } from './sharedConnectorSetup'
2222

23-
// ----- Entity model bridge -----
24-
25-
function InventoryEntityBridge ({ width, height }: { width: number; height: number }) {
26-
const ref = useRef<HTMLDivElement>(null)
27-
28-
useEffect(() => {
29-
const el = ref.current
30-
if (!el) return
31-
32-
const rect = el.getBoundingClientRect()
33-
const skinUrl = (appViewer?.playerState?.reactive as any)?.playerSkin ?? ''
34-
35-
modelViewerState.model = {
36-
steveModelSkin: skinUrl,
37-
positioning: {
38-
windowWidth: window.innerWidth,
39-
windowHeight: window.innerHeight,
40-
x: rect.left,
41-
y: rect.top,
42-
width: rect.width,
43-
height: rect.height,
44-
},
45-
zIndex: 1001,
46-
followCursor: true,
47-
followCursorCenter: {
48-
x: rect.left + rect.width / 2,
49-
y: rect.top + rect.height / 2,
50-
},
51-
}
52-
53-
return () => {
54-
modelViewerState.model = undefined
55-
}
56-
}, [width, height])
57-
58-
return <div ref={ref} style={{ width: '100%', height: '100%' }} />
59-
}
6023

6124
// ----- Inventory component -----
6225

@@ -149,7 +112,7 @@ export const Inventory = () => {
149112
}, [connector])
150113

151114
const renderEntity = useCallback((w: number, h: number) => {
152-
return <InventoryEntityBridge width={w} height={h} />
115+
return <PlayerModelViewer width={w} height={h} />
153116
}, [])
154117

155118
if (!inventoryType || !connector) return null
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { useEffect, useRef } from 'react'
2+
import * as THREE from 'three'
3+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
4+
import { createPlayerObject, applySkinToPlayerObject, PlayerObjectType } from '../../../renderer/viewer/lib/createPlayerObject'
5+
6+
const setupMaterialTransparency = (material: THREE.Material): void => {
7+
if (
8+
material instanceof THREE.MeshStandardMaterial ||
9+
material instanceof THREE.MeshBasicMaterial ||
10+
material instanceof THREE.MeshPhongMaterial
11+
) {
12+
const hasAlpha = material.alphaMap ||
13+
(material.opacity !== undefined && material.opacity < 1) ||
14+
(material.map && material.map.format === THREE.RGBAFormat)
15+
if (hasAlpha) {
16+
material.transparent = true
17+
material.alphaTest = 0.01
18+
material.side = THREE.DoubleSide
19+
} else {
20+
material.transparent = false
21+
material.side = THREE.FrontSide
22+
}
23+
material.needsUpdate = true
24+
}
25+
}
26+
27+
export function PlayerModelViewer ({ width, height }: { width: number; height: number }) {
28+
const containerRef = useRef<HTMLDivElement>(null)
29+
const stateRef = useRef<{
30+
renderer: THREE.WebGLRenderer
31+
camera: THREE.PerspectiveCamera
32+
scene: THREE.Scene
33+
controls: OrbitControls
34+
playerObject: PlayerObjectType
35+
wrapper: THREE.Object3D
36+
disposed: boolean
37+
} | null>(null)
38+
39+
useEffect(() => {
40+
const container = containerRef.current
41+
if (!container) return
42+
43+
// Scene
44+
const scene = new THREE.Scene()
45+
scene.background = null
46+
47+
// Camera
48+
const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000)
49+
camera.position.set(0, 0, 3)
50+
51+
// Renderer
52+
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
53+
renderer.useLegacyLights = false
54+
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
55+
renderer.setPixelRatio(window.devicePixelRatio || 1)
56+
renderer.setSize(width, height)
57+
container.appendChild(renderer.domElement)
58+
59+
// Controls
60+
const controls = new OrbitControls(camera, renderer.domElement)
61+
controls.minPolarAngle = Math.PI / 2
62+
controls.maxPolarAngle = Math.PI / 2
63+
controls.enableDamping = true
64+
controls.dampingFactor = 0.05
65+
66+
// Lights
67+
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 3)
68+
scene.add(ambientLight)
69+
const cameraLight = new THREE.PointLight(0xff_ff_ff, 0.6)
70+
camera.add(cameraLight)
71+
scene.add(camera)
72+
73+
// Player model
74+
const { playerObject, wrapper } = createPlayerObject({ scale: 1 })
75+
playerObject.ears.visible = false
76+
playerObject.cape.visible = false
77+
78+
wrapper.traverse((child) => {
79+
if (child instanceof THREE.Mesh && child.material) {
80+
if (Array.isArray(child.material)) {
81+
for (const mat of child.material) setupMaterialTransparency(mat)
82+
} else {
83+
setupMaterialTransparency(child.material)
84+
}
85+
}
86+
})
87+
88+
// Scale to fit camera view
89+
const box = new THREE.Box3().setFromObject(wrapper)
90+
const size = box.getSize(new THREE.Vector3())
91+
const center = box.getCenter(new THREE.Vector3())
92+
const cameraDistance = camera.position.z
93+
const fov = camera.fov * Math.PI / 180
94+
const visibleHeight = 2 * Math.tan(fov / 2) * cameraDistance
95+
const visibleWidth = visibleHeight * (width / height)
96+
const scaleFactor = Math.min(visibleHeight / size.y, visibleWidth / size.x)
97+
wrapper.scale.multiplyScalar(scaleFactor)
98+
wrapper.position.sub(center.multiplyScalar(scaleFactor))
99+
wrapper.rotation.set(0, 0, 0)
100+
scene.add(wrapper)
101+
102+
// Render helper
103+
const render = () => { renderer.render(scene, camera) }
104+
105+
// Apply skin
106+
const skinUrl = (appViewer?.playerState?.reactive as any)?.playerSkin ?? ''
107+
void applySkinToPlayerObject(playerObject, skinUrl).then(() => { render() })
108+
109+
// Render on orbit change
110+
controls.addEventListener('change', render)
111+
render()
112+
113+
// Cursor following
114+
let waitingRender = false
115+
const handlePointerMove = (event: PointerEvent) => {
116+
const el = containerRef.current
117+
if (!el) return
118+
119+
const rect = el.getBoundingClientRect()
120+
const centerX = rect.left + rect.width / 2
121+
const centerY = rect.top + rect.height / 2
122+
const normalizedX = (event.clientX - centerX) / (rect.width / 2)
123+
const normalizedY = (event.clientY - centerY) / (rect.height / 2)
124+
125+
const maxAngle = Math.PI * (60 / 180)
126+
const clampedX = THREE.MathUtils.clamp(normalizedX, -1, 1)
127+
const clampedY = THREE.MathUtils.clamp(normalizedY, -1, 1)
128+
const headYaw = clampedX * maxAngle
129+
const headPitch = clampedY * maxAngle
130+
131+
playerObject.skin.head.rotation.y = THREE.MathUtils.lerp(playerObject.skin.head.rotation.y, headYaw, 0.1)
132+
playerObject.skin.head.rotation.x = THREE.MathUtils.lerp(playerObject.skin.head.rotation.x, headPitch, 0.1)
133+
playerObject.rotation.y = THREE.MathUtils.lerp(playerObject.rotation.y, headYaw * 0.3, 0.05)
134+
135+
if (!waitingRender) {
136+
requestAnimationFrame(() => {
137+
render()
138+
waitingRender = false
139+
})
140+
waitingRender = true
141+
}
142+
}
143+
window.addEventListener('pointermove', handlePointerMove)
144+
145+
stateRef.current = { renderer, camera, scene, controls, playerObject, wrapper, disposed: false }
146+
147+
return () => {
148+
if (stateRef.current) stateRef.current.disposed = true
149+
window.removeEventListener('pointermove', handlePointerMove)
150+
controls.removeEventListener('change', render)
151+
controls.dispose()
152+
153+
wrapper.traverse((child) => {
154+
if (child instanceof THREE.Mesh) {
155+
if (Array.isArray(child.material)) {
156+
for (const mat of child.material) mat.dispose()
157+
} else {
158+
child.material?.dispose()
159+
}
160+
child.geometry?.dispose()
161+
}
162+
})
163+
if (playerObject.skin.map) {
164+
(playerObject.skin.map as unknown as THREE.Texture).dispose()
165+
}
166+
renderer.dispose()
167+
renderer.domElement?.remove()
168+
stateRef.current = null
169+
}
170+
}, [])
171+
172+
// Handle resize
173+
useEffect(() => {
174+
const s = stateRef.current
175+
if (!s || s.disposed) return
176+
s.renderer.setSize(width, height)
177+
s.camera.aspect = width / height
178+
s.camera.updateProjectionMatrix()
179+
s.renderer.render(s.scene, s.camera)
180+
}, [width, height])
181+
182+
return <div ref={containerRef} style={{ width, height, overflow: 'hidden', pointerEvents: 'auto' }} />
183+
}

0 commit comments

Comments
 (0)