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,38 @@
|
|
|
1
|
+
import PrismarineChatLoader from 'prismarine-chat'
|
|
2
|
+
import { renderSign } from '.'
|
|
3
|
+
|
|
4
|
+
const PrismarineChat = PrismarineChatLoader({ language: {} } as any)
|
|
5
|
+
|
|
6
|
+
const img = new Image()
|
|
7
|
+
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAMCAYAAAB4MH11AAABbElEQVR4AY3BQY6cMBBA0Q+yQZZVi+ndcJVcKGfMgegdvShKVtuokzGSWwwiUd7rfv388Vst0UgMXCobmgsSA5VaQmKgUks0EgNHji8SA9W8GJCQwVNpLhzJ4KFs4B1HEgPVvBiQkMFTaS44tYTEQDXdIkfiHbuyobmguaDPFzIWGrWExEA13SJH4h1uzS/WbPyvroM1v6jWbFRrNv7GfX5EdmXjzTvUEjJ4zjQXjiQGdmXjzTvUEjJ4HF/UEt/kQqW5UEkMzIshY08jg6dRS3yTC5XmgpsXY7pFztQSEgPNJCNv3lGpJVSfTLfImVpCYsB1HdwfxpU1G9eeNF0H94dxZc2G+/yI7MoG3vEv82LI2NNIDLyVDbzjzFE2mnkxZOy5IoNnkpFGc2FXNpp5MWTsOXJ4h1qikrGnkhjYlY1m1icy9lQSA+TCzjvUEpWMPZXEwK5suPvDOFuzcdZ1sOYX1ZqNas3GlTUbzR+jQbEAcs8ZQAAAAABJRU5ErkJggg=='
|
|
8
|
+
|
|
9
|
+
await new Promise<void>(resolve => {
|
|
10
|
+
img.onload = () => resolve()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const blockEntity = {
|
|
14
|
+
'GlowingText': 0,
|
|
15
|
+
'Color': 'black',
|
|
16
|
+
'Text4': '{"text":""}',
|
|
17
|
+
'Text3': '{"text":""}',
|
|
18
|
+
'Text2': '{"text":""}',
|
|
19
|
+
'Text1': '{"extra":[{"color":"dark_green","text":"Minecraft "},{"text":"Tools"}],"text":""}'
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
await document.fonts.load('1em mojangles')
|
|
23
|
+
|
|
24
|
+
const canvas = renderSign(blockEntity, false, PrismarineChat, (ctx) => {
|
|
25
|
+
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
|
|
26
|
+
}, (width, height) => {
|
|
27
|
+
const canvas = document.createElement('canvas')
|
|
28
|
+
canvas.width = width
|
|
29
|
+
canvas.height = height
|
|
30
|
+
return canvas as unknown as OffscreenCanvas
|
|
31
|
+
}) as unknown as HTMLCanvasElement
|
|
32
|
+
|
|
33
|
+
if (canvas) {
|
|
34
|
+
canvas.style.imageRendering = 'pixelated'
|
|
35
|
+
document.body.appendChild(canvas)
|
|
36
|
+
} else {
|
|
37
|
+
console.log('Render skipped')
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test, expect } from 'vitest'
|
|
2
|
+
import PrismarineChatLoader from 'prismarine-chat'
|
|
3
|
+
import { renderSign } from '.'
|
|
4
|
+
|
|
5
|
+
const PrismarineChat = PrismarineChatLoader({ language: {} } as any)
|
|
6
|
+
let ctxTexts = [] as any[]
|
|
7
|
+
|
|
8
|
+
globalThis.document = {
|
|
9
|
+
createElement() {
|
|
10
|
+
return {
|
|
11
|
+
getContext() {
|
|
12
|
+
return {
|
|
13
|
+
fillText(text, x, y) {
|
|
14
|
+
ctxTexts.push({ text, x, y })
|
|
15
|
+
},
|
|
16
|
+
measureText() { return 0 }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} as any
|
|
22
|
+
|
|
23
|
+
const render = (entity) => {
|
|
24
|
+
ctxTexts = []
|
|
25
|
+
renderSign(entity, true, PrismarineChat)
|
|
26
|
+
return ctxTexts.map(({ text, y }) => [y / 64, text])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('sign renderer', () => {
|
|
30
|
+
let blockEntity = {
|
|
31
|
+
'GlowingText': 0,
|
|
32
|
+
'Color': 'black',
|
|
33
|
+
'Text4': '{"text":""}',
|
|
34
|
+
'Text3': '{"text":""}',
|
|
35
|
+
'Text2': '{"text":""}',
|
|
36
|
+
'Text1': '{"extra":[{"color":"dark_green","text":"Minecraft "},{"text":"Tools"}],"text":""}'
|
|
37
|
+
} as any
|
|
38
|
+
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
|
39
|
+
[
|
|
40
|
+
[
|
|
41
|
+
1,
|
|
42
|
+
"Minecraft ",
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
1,
|
|
46
|
+
"Tools",
|
|
47
|
+
],
|
|
48
|
+
]
|
|
49
|
+
`)
|
|
50
|
+
|
|
51
|
+
blockEntity = { // pre flatenning
|
|
52
|
+
'Text1': 'Welcome to',
|
|
53
|
+
'Text2': '',
|
|
54
|
+
'Text3': 'null',
|
|
55
|
+
'Text4': '"Version 2.1"',
|
|
56
|
+
} as const
|
|
57
|
+
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
|
58
|
+
[
|
|
59
|
+
[
|
|
60
|
+
1,
|
|
61
|
+
"Welcome to",
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
4,
|
|
65
|
+
"Version 2.1",
|
|
66
|
+
],
|
|
67
|
+
]
|
|
68
|
+
`)
|
|
69
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BlockModel } from 'mc-assets/dist/types'
|
|
2
|
+
import { ItemSpecificContextProperties } from '@/playerState/types'
|
|
3
|
+
import { PlayerStateRenderer } from '@/playerState/playerState'
|
|
4
|
+
import { GeneralInputItem, getItemModelName } from '@/lib/items'
|
|
5
|
+
import { ResourcesManager, ResourcesManagerTransferred } from '@/resourcesManager'
|
|
6
|
+
import { renderSlot } from './renderSlot'
|
|
7
|
+
|
|
8
|
+
export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): {
|
|
9
|
+
u: number
|
|
10
|
+
v: number
|
|
11
|
+
su: number
|
|
12
|
+
sv: number
|
|
13
|
+
renderInfo?: ReturnType<typeof renderSlot>
|
|
14
|
+
// texture: ImageBitmap
|
|
15
|
+
modelName: string
|
|
16
|
+
} | {
|
|
17
|
+
resolvedModel: BlockModel
|
|
18
|
+
modelName: string
|
|
19
|
+
} => {
|
|
20
|
+
const resources = resourcesManager.currentResources
|
|
21
|
+
if (!resources) throw new Error('Resources not loaded')
|
|
22
|
+
const idOrName = item.itemId ?? item.blockId ?? item.name
|
|
23
|
+
const { blockState } = item
|
|
24
|
+
try {
|
|
25
|
+
const name =
|
|
26
|
+
blockState
|
|
27
|
+
? loadedData.blocksByStateId[blockState]?.name
|
|
28
|
+
: typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
|
|
29
|
+
if (!name) throw new Error(`Item not found: ${idOrName}`)
|
|
30
|
+
|
|
31
|
+
const model = getItemModelName({
|
|
32
|
+
...item,
|
|
33
|
+
name,
|
|
34
|
+
} as GeneralInputItem, specificProps, resourcesManager, playerState)
|
|
35
|
+
|
|
36
|
+
const renderInfo = renderSlot({
|
|
37
|
+
modelName: model,
|
|
38
|
+
}, resourcesManager, false, true)
|
|
39
|
+
|
|
40
|
+
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
|
41
|
+
|
|
42
|
+
const img = renderInfo.texture === 'blocks' ? resources.blocksAtlasImage : resources.itemsAtlasImage
|
|
43
|
+
|
|
44
|
+
if (renderInfo.blockData) {
|
|
45
|
+
return {
|
|
46
|
+
resolvedModel: renderInfo.blockData.resolvedModel,
|
|
47
|
+
modelName: renderInfo.modelName!
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (renderInfo.slice) {
|
|
51
|
+
// Get slice coordinates from either block or item texture
|
|
52
|
+
const [x, y, w, h] = renderInfo.slice
|
|
53
|
+
const [u, v, su, sv] = [x / img.width, y / img.height, (w / img.width), (h / img.height)]
|
|
54
|
+
return {
|
|
55
|
+
u, v, su, sv,
|
|
56
|
+
renderInfo,
|
|
57
|
+
// texture: img,
|
|
58
|
+
modelName: renderInfo.modelName!
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error(`Invalid render info for item ${name}`)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
reportError?.(err)
|
|
65
|
+
// Return default UV coordinates for missing texture
|
|
66
|
+
return {
|
|
67
|
+
u: 0,
|
|
68
|
+
v: 0,
|
|
69
|
+
su: 16 / resources.blocksAtlasImage.width,
|
|
70
|
+
sv: 16 / resources.blocksAtlasImage.width,
|
|
71
|
+
// texture: resources.blocksAtlasImage,
|
|
72
|
+
modelName: 'missing'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { Vec3 } from 'vec3'
|
|
3
|
+
import { createCanvas } from '../lib/utils'
|
|
4
|
+
import type { WorldRendererThree } from './worldRendererThree'
|
|
5
|
+
|
|
6
|
+
type BannerBlockEntity = {
|
|
7
|
+
Patterns?: Array<{
|
|
8
|
+
Color?: number
|
|
9
|
+
Pattern?: string
|
|
10
|
+
}>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Banner cloth size is 20x40
|
|
14
|
+
const BANNER_WIDTH = 20
|
|
15
|
+
const BANNER_HEIGHT = 40
|
|
16
|
+
|
|
17
|
+
// Map banner block names to base color IDs
|
|
18
|
+
const BANNER_NAME_TO_COLOR: Record<string, number> = {
|
|
19
|
+
'white_banner': 15,
|
|
20
|
+
'orange_banner': 14,
|
|
21
|
+
'magenta_banner': 13,
|
|
22
|
+
'light_blue_banner': 12,
|
|
23
|
+
'yellow_banner': 11,
|
|
24
|
+
'lime_banner': 10,
|
|
25
|
+
'pink_banner': 9,
|
|
26
|
+
'gray_banner': 8,
|
|
27
|
+
'light_gray_banner': 7,
|
|
28
|
+
'cyan_banner': 6,
|
|
29
|
+
'purple_banner': 5,
|
|
30
|
+
'blue_banner': 4,
|
|
31
|
+
'brown_banner': 3,
|
|
32
|
+
'green_banner': 2,
|
|
33
|
+
'red_banner': 1,
|
|
34
|
+
'black_banner': 0,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Basic Minecraft banner colors (DyeColor enum values)
|
|
38
|
+
const BANNER_COLORS: Record<number, string> = {
|
|
39
|
+
0: '#1d1d21', // black
|
|
40
|
+
1: '#b02e26', // red
|
|
41
|
+
2: '#5e7c16', // green
|
|
42
|
+
3: '#835432', // brown
|
|
43
|
+
4: '#3c44aa', // blue
|
|
44
|
+
5: '#8932b8', // purple
|
|
45
|
+
6: '#169c9c', // cyan
|
|
46
|
+
7: '#9d9d97', // light_gray
|
|
47
|
+
8: '#474f52', // gray
|
|
48
|
+
9: '#f38baa', // pink
|
|
49
|
+
10: '#80c71f', // lime
|
|
50
|
+
11: '#fed83d', // yellow
|
|
51
|
+
12: '#3ab3da', // light_blue
|
|
52
|
+
13: '#c74ebd', // magenta
|
|
53
|
+
14: '#f9801d', // orange
|
|
54
|
+
15: '#f9fffe', // white
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Extract base color from banner block name
|
|
58
|
+
function getBannerBaseColor(blockName: string): number {
|
|
59
|
+
// Remove _wall_banner suffix if present
|
|
60
|
+
const baseName = blockName.replace('_wall_banner', '_banner')
|
|
61
|
+
return BANNER_NAME_TO_COLOR[baseName] ?? 15 // Default to white
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Basic pattern rendering (simplified - just solid colors for now)
|
|
65
|
+
const renderPattern = (
|
|
66
|
+
ctx: OffscreenCanvasRenderingContext2D,
|
|
67
|
+
pattern: string,
|
|
68
|
+
color: string,
|
|
69
|
+
x: number,
|
|
70
|
+
y: number,
|
|
71
|
+
width: number,
|
|
72
|
+
height: number
|
|
73
|
+
) => {
|
|
74
|
+
ctx.fillStyle = color
|
|
75
|
+
// For now, just render basic patterns as solid colors
|
|
76
|
+
// TODO: Implement actual pattern shapes (stripes, crosses, etc.)
|
|
77
|
+
switch (pattern) {
|
|
78
|
+
case 'bs': // Base
|
|
79
|
+
ctx.fillRect(x, y, width, height)
|
|
80
|
+
break
|
|
81
|
+
case 'ls': // Left stripe
|
|
82
|
+
ctx.fillRect(x, y, width / 3, height)
|
|
83
|
+
break
|
|
84
|
+
case 'rs': // Right stripe
|
|
85
|
+
ctx.fillRect(x + (width * 2 / 3), y, width / 3, height)
|
|
86
|
+
break
|
|
87
|
+
case 'ts': // Top stripe
|
|
88
|
+
ctx.fillRect(x, y, width, height / 3)
|
|
89
|
+
break
|
|
90
|
+
case 'ms': // Middle stripe
|
|
91
|
+
ctx.fillRect(x, y + (height / 3), width, height / 3)
|
|
92
|
+
break
|
|
93
|
+
case 'drs': // Down-right stripe
|
|
94
|
+
ctx.fillRect(x, y, width / 2, height / 2)
|
|
95
|
+
break
|
|
96
|
+
case 'dls': // Down-left stripe
|
|
97
|
+
ctx.fillRect(x + (width / 2), y, width / 2, height / 2)
|
|
98
|
+
break
|
|
99
|
+
case 'ss': // Small stripes
|
|
100
|
+
for (let i = 0; i < width; i += 2) {
|
|
101
|
+
ctx.fillRect(x + i, y, 1, height)
|
|
102
|
+
}
|
|
103
|
+
break
|
|
104
|
+
case 'cr': // Cross
|
|
105
|
+
ctx.fillRect(x, y + (height / 3), width, height / 3)
|
|
106
|
+
ctx.fillRect(x + (width / 3), y, width / 3, height)
|
|
107
|
+
break
|
|
108
|
+
case 'sc': // Straight cross
|
|
109
|
+
ctx.fillRect(x, y + (height / 2) - 1, width, 2)
|
|
110
|
+
ctx.fillRect(x + (width / 2) - 1, y, 2, height)
|
|
111
|
+
break
|
|
112
|
+
default:
|
|
113
|
+
// Default: fill entire area
|
|
114
|
+
ctx.fillRect(x, y, width, height)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create a cache key from banner content (base color + patterns)
|
|
119
|
+
function createBannerCacheKey(baseColor: number, patterns: Array<{ Color?: number, Pattern?: string }> | undefined): string {
|
|
120
|
+
if (!patterns || patterns.length === 0) {
|
|
121
|
+
return `banner_${baseColor}_empty`
|
|
122
|
+
}
|
|
123
|
+
const patternStr = patterns.map(p => `${p.Pattern ?? 'bs'}_${p.Color ?? 0}`).join(',')
|
|
124
|
+
return `banner_${baseColor}_${patternStr}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const renderBanner = (
|
|
128
|
+
baseColor: number,
|
|
129
|
+
blockEntity: BannerBlockEntity,
|
|
130
|
+
canvasCreator = (width: number, height: number): OffscreenCanvas => {
|
|
131
|
+
return createCanvas(width, height)
|
|
132
|
+
}
|
|
133
|
+
) => {
|
|
134
|
+
// Create canvas with banner cloth size (20x40)
|
|
135
|
+
const scale = 1
|
|
136
|
+
const canvas = canvasCreator(BANNER_WIDTH * scale, BANNER_HEIGHT * scale)
|
|
137
|
+
const ctx = canvas.getContext('2d')!
|
|
138
|
+
|
|
139
|
+
if (!ctx) {
|
|
140
|
+
console.warn('Failed to get 2d context for banner rendering')
|
|
141
|
+
return undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ctx.imageSmoothingEnabled = false
|
|
145
|
+
|
|
146
|
+
// Always render base color first (even if no patterns)
|
|
147
|
+
const baseColorHex = BANNER_COLORS[baseColor] || BANNER_COLORS[15]
|
|
148
|
+
ctx.fillStyle = baseColorHex
|
|
149
|
+
ctx.fillRect(0, 0, BANNER_WIDTH * scale, BANNER_HEIGHT * scale)
|
|
150
|
+
|
|
151
|
+
// Render patterns on top of base color (if any)
|
|
152
|
+
if (blockEntity?.Patterns && blockEntity.Patterns.length > 0) {
|
|
153
|
+
for (const patternData of blockEntity.Patterns) {
|
|
154
|
+
const colorId = patternData.Color ?? 0
|
|
155
|
+
const pattern = patternData.Pattern ?? 'bs'
|
|
156
|
+
const color = BANNER_COLORS[colorId] || BANNER_COLORS[0]
|
|
157
|
+
|
|
158
|
+
// Render each pattern on top of previous ones
|
|
159
|
+
renderPattern(
|
|
160
|
+
ctx,
|
|
161
|
+
pattern,
|
|
162
|
+
color,
|
|
163
|
+
0,
|
|
164
|
+
0,
|
|
165
|
+
BANNER_WIDTH * scale,
|
|
166
|
+
BANNER_HEIGHT * scale
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return canvas
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// Banner texture cache with reference counting
|
|
176
|
+
const bannerTextureCache = new Map<string, { texture: THREE.Texture, refCount: number }>()
|
|
177
|
+
|
|
178
|
+
export function getBannerTexture(
|
|
179
|
+
worldRenderer: WorldRendererThree,
|
|
180
|
+
blockName: string,
|
|
181
|
+
blockEntity: any
|
|
182
|
+
): THREE.Texture | undefined {
|
|
183
|
+
// Extract base color from block name
|
|
184
|
+
const baseColor = getBannerBaseColor(blockName)
|
|
185
|
+
|
|
186
|
+
// Create cache key from banner content (not position)
|
|
187
|
+
const cacheKey = createBannerCacheKey(baseColor, blockEntity?.Patterns)
|
|
188
|
+
|
|
189
|
+
// Check cache
|
|
190
|
+
const cached = bannerTextureCache.get(cacheKey)
|
|
191
|
+
if (cached) {
|
|
192
|
+
cached.refCount++
|
|
193
|
+
return cached.texture
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Render new banner
|
|
197
|
+
const canvas = renderBanner(baseColor, blockEntity)
|
|
198
|
+
if (!canvas) return undefined
|
|
199
|
+
|
|
200
|
+
const tex = new THREE.Texture(canvas)
|
|
201
|
+
tex.magFilter = THREE.NearestFilter
|
|
202
|
+
tex.minFilter = THREE.NearestFilter
|
|
203
|
+
tex.needsUpdate = true
|
|
204
|
+
|
|
205
|
+
// Store in cache with reference count
|
|
206
|
+
bannerTextureCache.set(cacheKey, { texture: tex, refCount: 1 })
|
|
207
|
+
return tex
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function releaseBannerTexture(texture: THREE.Texture): void {
|
|
211
|
+
// Find and decrement reference count
|
|
212
|
+
for (const [key, cached] of bannerTextureCache.entries()) {
|
|
213
|
+
if (cached.texture === texture) {
|
|
214
|
+
cached.refCount--
|
|
215
|
+
if (cached.refCount <= 0) {
|
|
216
|
+
// Cleanup unused texture
|
|
217
|
+
cached.texture.dispose()
|
|
218
|
+
bannerTextureCache.delete(key)
|
|
219
|
+
}
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function createBannerMesh(
|
|
226
|
+
position: Vec3,
|
|
227
|
+
rotation: number,
|
|
228
|
+
isWall: boolean,
|
|
229
|
+
texture: THREE.Texture
|
|
230
|
+
): THREE.Group & { bannerTexture?: THREE.Texture } {
|
|
231
|
+
const bannerWidth = 13.6 / 16
|
|
232
|
+
const bannerHeight = 28 / 16
|
|
233
|
+
const clothXOffset = 0
|
|
234
|
+
|
|
235
|
+
let clothYOffset: number
|
|
236
|
+
let clothZPosition: number
|
|
237
|
+
let heightOffset: number
|
|
238
|
+
|
|
239
|
+
if (isWall) {
|
|
240
|
+
// Wall banner: Cloth from [1.2, -14.6, 14.5] to [14.8, 13.4, 15]
|
|
241
|
+
clothYOffset = (-14.6 + 13.4) / 2 / 16 - 0.5
|
|
242
|
+
clothZPosition = 1 - 14.75 / 16 - 0.5
|
|
243
|
+
heightOffset = 1 / 2
|
|
244
|
+
} else {
|
|
245
|
+
// Standing banner: Cloth from [1.2, 1.4, 7] to [14.8, 29.4, 7.5]
|
|
246
|
+
clothYOffset = (1.4 + 29.4) / 2 / 16
|
|
247
|
+
clothZPosition = 1 - 7.25 / 16 - 0.5
|
|
248
|
+
heightOffset = 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const mesh = new THREE.Mesh(
|
|
252
|
+
new THREE.PlaneGeometry(bannerWidth, bannerHeight),
|
|
253
|
+
new THREE.MeshBasicMaterial({ map: texture, transparent: true })
|
|
254
|
+
)
|
|
255
|
+
mesh.renderOrder = 999
|
|
256
|
+
|
|
257
|
+
const thickness = 0.5 / 16
|
|
258
|
+
const wallSpacing = 0.25 / 16
|
|
259
|
+
if (isWall) {
|
|
260
|
+
mesh.position.set(clothXOffset, clothYOffset, clothZPosition + wallSpacing + 0.004)
|
|
261
|
+
} else {
|
|
262
|
+
mesh.position.set(clothXOffset, clothYOffset, clothZPosition + thickness / 2 + 0.004)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const group = new THREE.Group() as THREE.Group & { bannerTexture?: THREE.Texture }
|
|
266
|
+
group.rotation.set(
|
|
267
|
+
0,
|
|
268
|
+
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
269
|
+
0
|
|
270
|
+
)
|
|
271
|
+
group.add(mesh)
|
|
272
|
+
group.bannerTexture = texture
|
|
273
|
+
group.position.set(position.x + 0.5, position.y + heightOffset, position.z + 0.5)
|
|
274
|
+
return group
|
|
275
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { WorldRendererThree } from './worldRendererThree'
|
|
3
|
+
|
|
4
|
+
export class CameraShake {
|
|
5
|
+
private rollAngle = 0
|
|
6
|
+
private get damageRollAmount() { return 5 }
|
|
7
|
+
private get damageAnimDuration() { return 200 }
|
|
8
|
+
private rollAnimation?: { startTime: number, startRoll: number, targetRoll: number, duration: number, returnToZero?: boolean }
|
|
9
|
+
private basePitch = 0
|
|
10
|
+
private baseYaw = 0
|
|
11
|
+
|
|
12
|
+
constructor(public worldRenderer: WorldRendererThree, public onRenderCallbacks: Array<() => void>) {
|
|
13
|
+
onRenderCallbacks.push(() => {
|
|
14
|
+
this.update()
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setBaseRotation(pitch: number, yaw: number) {
|
|
19
|
+
this.basePitch = pitch
|
|
20
|
+
this.baseYaw = yaw
|
|
21
|
+
this.update()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getBaseRotation() {
|
|
25
|
+
return { pitch: this.basePitch, yaw: this.baseYaw }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
shakeFromDamage(yaw?: number) {
|
|
29
|
+
// Add roll animation
|
|
30
|
+
const startRoll = this.rollAngle
|
|
31
|
+
const targetRoll = startRoll + (yaw ?? (Math.random() < 0.5 ? -1 : 1)) * this.damageRollAmount
|
|
32
|
+
|
|
33
|
+
this.rollAnimation = {
|
|
34
|
+
startTime: performance.now(),
|
|
35
|
+
startRoll,
|
|
36
|
+
targetRoll,
|
|
37
|
+
duration: this.damageAnimDuration / 2
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
update() {
|
|
42
|
+
if (this.worldRenderer.playerStateUtils.isSpectatingEntity()) {
|
|
43
|
+
// Remove any shaking when spectating
|
|
44
|
+
this.rollAngle = 0
|
|
45
|
+
this.rollAnimation = undefined
|
|
46
|
+
}
|
|
47
|
+
// Update roll animation
|
|
48
|
+
if (this.rollAnimation) {
|
|
49
|
+
const now = performance.now()
|
|
50
|
+
const elapsed = now - this.rollAnimation.startTime
|
|
51
|
+
const progress = Math.min(elapsed / this.rollAnimation.duration, 1)
|
|
52
|
+
|
|
53
|
+
if (this.rollAnimation.returnToZero) {
|
|
54
|
+
// Ease back to zero
|
|
55
|
+
this.rollAngle = this.rollAnimation.startRoll * (1 - this.easeInOut(progress))
|
|
56
|
+
if (progress === 1) {
|
|
57
|
+
this.rollAnimation = undefined
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// Initial roll
|
|
61
|
+
this.rollAngle = this.rollAnimation.startRoll + (this.rollAnimation.targetRoll - this.rollAnimation.startRoll) * this.easeOut(progress)
|
|
62
|
+
if (progress === 1) {
|
|
63
|
+
// Start return to zero animation
|
|
64
|
+
this.rollAnimation = {
|
|
65
|
+
startTime: now,
|
|
66
|
+
startRoll: this.rollAngle,
|
|
67
|
+
targetRoll: 0,
|
|
68
|
+
duration: this.damageAnimDuration / 2,
|
|
69
|
+
returnToZero: true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const camera = this.worldRenderer.cameraObject
|
|
76
|
+
|
|
77
|
+
if (this.worldRenderer.cameraGroupVr) {
|
|
78
|
+
// For VR camera, only apply yaw rotation
|
|
79
|
+
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw)
|
|
80
|
+
camera.setRotationFromQuaternion(yawQuat)
|
|
81
|
+
} else {
|
|
82
|
+
// For regular camera, apply all rotations
|
|
83
|
+
// Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees)
|
|
84
|
+
const pitchOffset = this.addAntiZfightingOffset(this.basePitch)
|
|
85
|
+
const yawOffset = this.addAntiZfightingOffset(this.baseYaw)
|
|
86
|
+
|
|
87
|
+
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset)
|
|
88
|
+
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset)
|
|
89
|
+
const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
|
|
90
|
+
// Combine rotations in the correct order: pitch -> yaw -> roll
|
|
91
|
+
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
|
|
92
|
+
camera.setRotationFromQuaternion(finalQuat)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private easeOut(t: number): number {
|
|
97
|
+
return 1 - (1 - t) * (1 - t)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private easeInOut(t: number): number {
|
|
101
|
+
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private addAntiZfightingOffset(angle: number): number {
|
|
105
|
+
const offset = 0.001 // Very small offset in radians (about 0.057 degrees)
|
|
106
|
+
|
|
107
|
+
// Check if the angle is close to ideal angles (0, π/2, π, 3π/2)
|
|
108
|
+
const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)
|
|
109
|
+
const tolerance = 0.01 // Tolerance for considering an angle "ideal"
|
|
110
|
+
|
|
111
|
+
if (Math.abs(normalizedAngle) < tolerance ||
|
|
112
|
+
Math.abs(normalizedAngle - Math.PI / 2) < tolerance ||
|
|
113
|
+
Math.abs(normalizedAngle - Math.PI) < tolerance ||
|
|
114
|
+
Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) {
|
|
115
|
+
return angle + offset
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return angle
|
|
119
|
+
}
|
|
120
|
+
}
|