Records wear real sleeves: Nano Banana art atlases + UV-tile skinning

- assets/art/sleeves_{a,b}.jpg: two 3x3 sheets of ORIGINAL fictional album
  covers (18 total — dub, techno, metal, jazz, punk, synthwave, hip-hop,
  psych, ambient, northern soul, krautrock, disco, white-label, bossa,
  UKG, country, noise) generated with Gemini gemini-3-pro-image. No real
  artists/albums — copyright-clean destructible content.
- assets/art/keyart.jpg: DESTROYULATOR title key art ("CLOCK OUT. SMASH
  EVERYTHING.") for a future title screen.
- Main.gd _skin_record(): records duplicate their M_Sleeve_Front/Back
  materials and window one random atlas tile via uv1_scale/uv1_offset —
  one texture in VRAM serves nine covers, every record looks different.
- Verified headless: boots clean, "[art] records skinned: 15" (all of
  them). The two dummy-renderer "material is null" prints are headless-
  only (override materials have no RID in the dummy backend). Eyeball a
  sleeve in the editor — if the art renders rotated, flip uv1 here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Monster Robot Party 2026-07-07 23:24:11 +10:00
parent 7c88e8371d
commit b78ea04183
7 changed files with 170 additions and 0 deletions

BIN
assets/art/keyart.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://drqh8c73yn7c3"
path="res://.godot/imported/keyart.jpg-a17068a646e54c982c021f2d854b8d99.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/art/keyart.jpg"
dest_files=["res://.godot/imported/keyart.jpg-a17068a646e54c982c021f2d854b8d99.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/art/sleeves_a.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dkqsrlw0qbq8q"
path="res://.godot/imported/sleeves_a.jpg-c3eb7828a71203698e54664c9ba6c8ac.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/art/sleeves_a.jpg"
dest_files=["res://.godot/imported/sleeves_a.jpg-c3eb7828a71203698e54664c9ba6c8ac.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/art/sleeves_b.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dxq5sjamvdegc"
path="res://.godot/imported/sleeves_b.jpg-5f25abc3a10a57693e8df03cba6dbd6d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/art/sleeves_b.jpg"
dest_files=["res://.godot/imported/sleeves_b.jpg-5f25abc3a10a57693e8df03cba6dbd6d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -82,6 +82,16 @@ const RACK_GLB := "res://assets/store/rack.glb"
const CRATE_GLB := "res://assets/store/crate.glb"
const RECORD_GLB := "res://assets/store/record.glb"
# Generated sleeve art (Nano Banana): each file is a 3x3 sheet of nine original
# fictional album covers. A record picks one tile at random via uv1 scale/offset —
# one texture in VRAM, eighteen different-looking records on the floor.
const SLEEVE_ATLASES: Array[String] = [
"res://assets/art/sleeves_a.jpg",
"res://assets/art/sleeves_b.jpg",
]
var _sleeve_textures: Array[Texture2D] = []
var _records_skinned := 0
func _build_rack(origin: Vector3) -> void:
# two racks form a smashable back wall of the booth
_glb_piece(RACK_GLB, "wood", Vector2(origin.x - 0.95, origin.z - 0.7), true)
@ -101,6 +111,7 @@ func _build_rack(origin: Vector3) -> void:
var jitter := Vector2(randf_range(-0.06, 0.06), randf_range(-0.06, 0.06))
var rec := _glb_piece(RECORD_GLB, "vinyl", xz + jitter, false, crate_top + float(i) * 0.03)
(crate["piece"] as Smashable).supports.append(rec["piece"])
print("[art] records skinned with sleeve art: ", _records_skinned)
## Instance a GLB as the VISUAL of a Smashable, give it a box collider auto-sized to
## the mesh AABB, and drop it so its bottom rests at height `sit_on`. Returns
@ -112,6 +123,8 @@ func _glb_piece(path: String, kind: String, xz: Vector2, frozen: bool, sit_on :=
var packed := load(path) as PackedScene
var vis: Node3D = packed.instantiate()
s.add_child(vis)
if kind == "vinyl":
_skin_record(vis) # random sleeve art on the jacket faces
_world.add_child(s) # in-tree so transforms/AABBs resolve
var ab := _body_local_aabb(s, vis)
if ab.size == Vector3.ZERO:
@ -126,6 +139,43 @@ func _glb_piece(path: String, kind: String, xz: Vector2, frozen: bool, sit_on :=
s.smashed.connect(_on_smashed)
return {"piece": s, "top": sit_on + ab.size.y}
## Give a record's sleeve faces a random cover from the generated 3x3 sheets.
## The record.glb sleeve cube carries M_Sleeve_Front / M_Sleeve_Back materials
## with real UVs — we duplicate them and window one tile of the atlas via
## uv1_scale/uv1_offset (no image slicing, one texture serves nine covers).
func _skin_record(vis: Node3D) -> void:
if _sleeve_textures.is_empty():
for p in SLEEVE_ATLASES:
var t := load(p) as Texture2D
if t != null:
_sleeve_textures.append(t)
if _sleeve_textures.is_empty():
return # art not imported yet — records stay plain
var tex: Texture2D = _sleeve_textures[randi() % _sleeve_textures.size()]
var tile_col := randi() % 3
var tile_row := randi() % 3
var mis: Array[MeshInstance3D] = []
_collect_meshes(vis, mis)
var did := false
for mi in mis:
if mi.mesh == null:
continue
for si in mi.mesh.get_surface_count():
var mat := mi.mesh.surface_get_material(si)
if mat == null or not (mat is BaseMaterial3D):
continue
var nm: String = mat.resource_name
if nm.contains("Sleeve_Front") or nm.contains("Sleeve_Back"):
var dup := mat.duplicate() as BaseMaterial3D
dup.albedo_color = Color.WHITE
dup.albedo_texture = tex
dup.uv1_scale = Vector3(1.0 / 3.0, 1.0 / 3.0, 1.0)
dup.uv1_offset = Vector3(tile_col / 3.0, tile_row / 3.0, 0.0)
mi.set_surface_override_material(si, dup)
did = true
if did:
_records_skinned += 1
## Combined AABB of every MeshInstance3D under `visual_root`, in `body`'s local space
## (independent of where `body` currently sits in the world).
func _body_local_aabb(body: Node3D, visual_root: Node3D) -> AABB: