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,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,10 @@
1
+ import { defineConfig } from 'vite'
2
+
3
+ export default defineConfig({
4
+ resolve: {
5
+ alias: {
6
+ 'prismarine-registry': './noop.js',
7
+ 'prismarine-nbt': './noop.js'
8
+ },
9
+ },
10
+ })
@@ -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
+ }