Skip to content

Commit 5e447b4

Browse files
authored
Release (#453)
2 parents 710b022 + 231cdce commit 5e447b4

File tree

9 files changed

+336
-5
lines changed

9 files changed

+336
-5
lines changed

renderer/viewer/lib/mesher/models.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
541541
// todo this can be removed here
542542
heads: {},
543543
signs: {},
544+
banners: {},
544545
// isFull: true,
545546
hadErrors: false,
546547
blocksCount: 0
@@ -552,7 +553,7 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
552553
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
553554
let block = world.getBlock(cursor, blockProvider, attr)!
554555
if (INVISIBLE_BLOCKS.has(block.name)) continue
555-
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
556+
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableBlockEntityTextures) {
556557
const key = `${cursor.x},${cursor.y},${cursor.z}`
557558
const props: any = block.getProperties()
558559
const facingRotationMap = {
@@ -582,6 +583,21 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
582583
isWall,
583584
rotation: isWall ? facingRotationMap[props.facing] : +props.rotation
584585
}
586+
} else if (block.name.includes('_banner') && !world.config.disableBlockEntityTextures) {
587+
const key = `${cursor.x},${cursor.y},${cursor.z}`
588+
const props: any = block.getProperties()
589+
const facingRotationMap = {
590+
'north': 2,
591+
'south': 0,
592+
'west': 1,
593+
'east': 3
594+
}
595+
const isWall = block.name.endsWith('_wall_banner')
596+
attr.banners[key] = {
597+
isWall,
598+
blockName: block.name, // Pass block name for base color extraction
599+
rotation: isWall ? facingRotationMap[props.facing] : (props.rotation === undefined ? 0 : +props.rotation)
600+
}
585601
}
586602
const biome = block.biome.name
587603

renderer/viewer/lib/mesher/shared.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const defaultMesherConfig = {
1212
// textureSize: 1024, // for testing
1313
debugModelVariant: undefined as undefined | number[],
1414
clipWorldBelowY: undefined as undefined | number,
15-
disableSignsMapsSupport: false
15+
disableBlockEntityTextures: false
1616
}
1717

1818
export type CustomBlockModels = {
@@ -41,6 +41,7 @@ export type MesherGeometryOutput = {
4141
tiles: Record<string, BlockType>,
4242
heads: Record<string, any>,
4343
signs: Record<string, any>,
44+
banners: Record<string, any>,
4445
// isFull: boolean
4546
hadErrors: boolean
4647
blocksCount: number

renderer/viewer/lib/worldrendererCommon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
585585
// textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
586586
debugModelVariant: this.worldRendererConfig.debugModelVariant,
587587
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
588-
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
588+
disableBlockEntityTextures: !this.worldRendererConfig.extraBlockRenderers,
589589
worldMinY: this.worldMinYRender,
590590
worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight,
591591
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

renderer/viewer/three/worldrendererThree.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { addNewStat } from '../lib/ui/newStats'
1212
import { MesherGeometryOutput } from '../lib/mesher/shared'
1313
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
1414
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
15+
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
1516
import { getMyHand } from './hand'
1617
import HoldingBlock from './holdingBlock'
1718
import { getMesh } from './entity/EntityMesh'
@@ -384,6 +385,12 @@ export class WorldRendererThree extends WorldRendererCommon {
384385
if (data.type !== 'geometry') return
385386
let object: THREE.Object3D = this.sectionObjects[data.key]
386387
if (object) {
388+
// Cleanup banner textures before disposing
389+
object.traverse((child) => {
390+
if ((child as any).bannerTexture) {
391+
releaseBannerTexture((child as any).bannerTexture)
392+
}
393+
})
387394
this.scene.remove(object)
388395
disposeObject(object)
389396
delete this.sectionObjects[data.key]
@@ -441,6 +448,17 @@ export class WorldRendererThree extends WorldRendererCommon {
441448
object.add(head)
442449
}
443450
}
451+
if (Object.keys(data.geometry.banners).length) {
452+
for (const [posKey, { isWall, rotation, blockName }] of Object.entries(data.geometry.banners)) {
453+
const bannerBlockEntity = this.blockEntities[posKey]
454+
if (!bannerBlockEntity) continue
455+
const [x, y, z] = posKey.split(',')
456+
const bannerTexture = getBannerTexture(this, blockName, nbt.simplify(bannerBlockEntity))
457+
if (!bannerTexture) continue
458+
const banner = createBannerMesh(new Vec3(+x, +y, +z), rotation, isWall, bannerTexture)
459+
object.add(banner)
460+
}
461+
}
444462
this.sectionObjects[data.key] = object
445463
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
446464
object.visible = false
@@ -960,6 +978,12 @@ export class WorldRendererThree extends WorldRendererCommon {
960978
const key = `${x},${y},${z}`
961979
const mesh = this.sectionObjects[key]
962980
if (mesh) {
981+
// Cleanup banner textures before disposing
982+
mesh.traverse((child) => {
983+
if ((child as any).bannerTexture) {
984+
releaseBannerTexture((child as any).bannerTexture)
985+
}
986+
})
963987
this.scene.remove(mesh)
964988
disposeObject(mesh)
965989
}

0 commit comments

Comments
 (0)