-
Notifications
You must be signed in to change notification settings - Fork 0
3.8 Implementing the Texture Generator
The goal of this task is to generate a 16x16 pixel texture for each item in the blueprint, where the texture's shape, color, and pattern are determined by the visual field in the blueprint. Additionally, to make textures automatically match the user's mod requirements, this task also requires preliminary modifications to Task 1 (System Prompt) and Task 2 (Schema), so that the LLM proactively infers the visual field when generating a blueprint, instead of waiting for the user to mention textures.
Note: This task includes modifications to the artifacts of previous tasks. This is not about going back to modify old documents, but rather consolidating the necessary adjustments here when implementing Task 8.
- Completed Task 7:
modsmith/generator/resources.pyhas already created thetextures/itemdirectory. - Pillow is installed (already declared in
pyproject.toml; if not, runpip install pillow).
Each item's texture is determined by three dimensions:
| Dimension | Description | Example |
|---|---|---|
| shape | The item's basic outline |
round (fruit), blade (sword), crystal (gem) |
| primary_color | Main color (hex) |
#dc2626 (red) |
| accent_color | Secondary color (highlight, outline) |
#fca5a5 (light red) |
| pattern | Overlay texture |
sparkle, glow
|
shape enum (9 values):
| Shape | Applicable type | Example item |
|---|---|---|
round |
Food, fruit | Apple, berry |
blade |
Tool (sword, dagger) | Sword |
axe |
Tool (axe) | Axe |
pickaxe |
Tool (pickaxe) | Pickaxe |
crystal |
Gem, ore | Ruby, diamond |
ingot |
Metal ingot | Iron ingot, gold ingot |
liquid |
Bottled liquid | Potion, fuel |
organic |
Other food | Bread, meat |
abstract |
Cannot be categorized | Mysterious substance |
pattern enum (6 values): none, sparkle, glow, cracks, dots, stripes.
Fallback mechanism: If an item has no visual field (for example, an old blueprint), the procedural generator falls back to the MD5 hash scheme and produces a colored square. Zero breakage for existing pipelines.
Problem: If the System Prompt only says "the visual field is required" but the user's description does not include texture information, the LLM may output "visual": {} or omit it entirely, causing the texture generator to fall back to the fallback scheme, resulting in textures that still lack distinctiveness.
Solution: Append a "visual inference" rule to the System Prompt in Task 1, so that the LLM proactively infers suitable visual features based on the item's name, type, and effect. At the same time, in Task 2's Schema, make visual required, using validation to force the LLM to output it every time.
Open _build_system_prompt() in modsmith/llm/client.py, and append the following content after the existing visual feature requirements:
## Visual features must be proactively inferred by you
When users describe a mod, they usually don't mention what the texture looks like. **You must infer a reasonable visual field based on the item's name, type, and effect.** Do not leave it empty, and do not output "auto".
Inference method:
1. **First look at name keywords**:
- Apple, berry, fruit, bread, meat → shape = round / organic
- Gem, crystal, diamond, ore, shard → shape = crystal
- Sword, blade, dagger → shape = blade
- Axe, pickaxe → shape = axe / pickaxe
- Potion, plasma, liquid, fuel → shape = liquid
- Ingot, block, metal → shape = ingot
2. **Then look at effects to infer color**:
- Healing, regeneration → red family (#dc2626)
- Speed, night vision → cyan family (#22d3ee)
- Poison, wither → dark green / purple family (#4d7c0f / #7e22ce)
- Strength, attack → orange-red family (#ea580c)
- Luck, fortune → gold family (#f59e0b)
- Cold, frost → blue-white family (#93c5fd)
3. **Finally look at name adjectives**:
- Sparkling, glowing → pattern = sparkle
- Mysterious, shadow → pattern = glow
- Cracked, ancient → pattern = cracks
- Pure, smooth → pattern = none
4. **Fallback**: If none of the above can be inferred, use shape = abstract and infer primary_color from the item ID's hash.
Examples:
User input "Create an apple that heals when eaten"
→ Name contains "apple" → shape = round
→ Effect is "heal" → primary_color = #dc2626
→ accent_color = #86efac (the green highlight of the fruit)
→ pattern = sparkle
User input "Make a sparkling diamond"
→ Name contains "diamond" → shape = crystal
→ Name contains "sparkling" → pattern = sparkle
→ primary_color = #22d3ee (the cyan-blue of diamond)Effect: The LLM will automatically infer suitable shapes and colors for descriptions like "healing apple," "mysterious potion," and "diamond sword," without the user needing to mention the texture in the description.
Open modsmith/blueprint/schema.json, and find the item object's required array and properties:
Original (visual optional):
"required": ["id", "type", "display_name_en"],
"properties": {
"id": { ... },
"type": { ... },
"visual": {
"type": "object",
"properties": {
"shape": { ... },
"primary_color": { ... },
"accent_color": { ... },
"pattern": { ... },
"notes": { ... }
}
}
}Change to (visual required, and its internal shape and primary_color also required):
"required": ["id", "type", "display_name_en", "visual"],
"properties": {
"id": { ... },
"type": { ... },
"visual": {
"type": "object",
"required": ["shape", "primary_color"],
"properties": {
"shape": {
"type": "string",
"enum": ["round", "blade", "axe", "pickaxe", "crystal",
"ingot", "liquid", "organic", "abstract"]
},
"primary_color": {
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$"
},
"accent_color": {
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$"
},
"pattern": {
"type": "string",
"enum": ["none", "sparkle", "glow", "cracks", "dots", "stripes"],
"default": "none"
},
"notes": { "type": "string" }
}
}
}Effect: If the LLM tries to omit visual, Schema validation will fail and trigger a retry until it fills it in. accent_color and pattern remain optional, because they can reasonably be omitted.
Replace the file content with the following complete code:
"""Generate item textures: draw pixel art of different shapes based on visual features."""
import hashlib
from pathlib import Path
from PIL import Image, ImageDraw
# ============================================================
# Utility functions
# ============================================================
def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
"""Convert '#dc2626' to (220, 38, 38)."""
hex_color = hex_color.lstrip("#")
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def _color_from_id(item_id: str) -> tuple[int, int, int]:
"""Fallback: generate a deterministic color from the item ID."""
digest = hashlib.md5(item_id.encode("utf-8")).hexdigest()
return (int(digest[0:2], 16), int(digest[2:4], 16), int(digest[4:6], 16))
def _get_visual(item: dict) -> dict:
"""Extract visual features from the item definition, using fallbacks when missing.
- If there is no primary_color, generate one from the item ID's MD5 hash.
- If there is no accent_color, use a lighter version of the primary color.
- If there is no shape, use "abstract".
- If there is no pattern, use "none".
"""
visual = dict(item.get("visual", {}))
if not visual.get("primary_color"):
r, g, b = _color_from_id(item["id"])
visual["primary_color"] = f"#{r:02x}{g:02x}{b:02x}"
if not visual.get("accent_color"):
r, g, b = _hex_to_rgb(visual["primary_color"])
visual["accent_color"] = (
f"#{min(r+60,255):02x}{min(g+60,255):02x}{min(b+60,255):02x}"
)
visual.setdefault("shape", "abstract")
visual.setdefault("pattern", "none")
return visual
# ============================================================
# Shape drawing functions (each returns a 16x16 RGBA Image)
# ============================================================
def _draw_round(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Round/fruit: a circle with a highlight."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
margin = 2
draw.ellipse(
[margin, margin, size - margin - 1, size - margin - 1],
fill=primary, outline=accent,
)
draw.ellipse([4, 4, 6, 6], fill=(255, 255, 255, 200))
return img
def _draw_blade(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Sword/dagger: diagonal blade + hilt."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.line([(4, size - 5), (size - 3, 4)], fill=primary, width=3)
draw.line([(5, size - 6), (size - 4, 3)], fill=accent, width=1)
draw.line([(2, size - 3), (5, size - 6)], fill=(101, 67, 33), width=2)
draw.line([(3, size - 8), (8, size - 3)], fill=accent, width=1)
return img
def _draw_axe(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Axe: wooden handle + axe head."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.line([(5, size - 2), (size - 5, 4)], fill=(101, 67, 33), width=2)
draw.polygon([(4, 4), (10, 2), (12, 7), (7, 9)], fill=primary, outline=accent)
return img
def _draw_pickaxe(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Pickaxe: wooden handle + pick head."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.line([(5, size - 2), (size - 5, 4)], fill=(101, 67, 33), width=2)
draw.arc([2, 2, size - 4, 10], start=180, end=360, fill=primary, width=2)
draw.arc([3, 3, size - 5, 9], start=180, end=360, fill=accent, width=1)
return img
def _draw_crystal(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Gem/ore: rhombus crystal with facet highlights."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
center = size // 2
draw.polygon([
(center, 2), (size - 3, center),
(center, size - 3), (3, center),
], fill=primary, outline=accent)
draw.polygon([
(center, 3), (center + 3, center), (center, center + 1),
], fill=(255, 255, 255, 120))
return img
def _draw_ingot(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Metal ingot: a 3D rectangular block."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.polygon(
[(3, 6), (size - 4, 5), (size - 3, 9), (2, 10)],
fill=accent, outline=primary,
)
draw.polygon(
[(2, 10), (size - 3, 9), (size - 4, size - 3), (3, size - 2)],
fill=primary, outline=accent,
)
return img
def _draw_liquid(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Bottled liquid: bottle outline + liquid."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rectangle([5, 7, size - 6, size - 3], fill=primary, outline=accent)
draw.rectangle([7, 3, size - 8, 7], fill=accent)
draw.rectangle([6, 2, size - 7, 3], fill=(101, 67, 33))
return img
def _draw_organic(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Organic food: irregular blocky shape."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.ellipse([2, 3, size - 3, size - 2], fill=primary, outline=accent)
draw.ellipse([4, 5, 7, 8], fill=(255, 255, 255, 100))
return img
def _draw_abstract(size: int, primary: tuple, accent: tuple) -> Image.Image:
"""Abstract: placeholder square with a dot in the center."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rectangle([1, 1, size - 2, size - 2], fill=primary, outline=accent)
center = size // 2
draw.ellipse(
[center - 2, center - 2, center + 2, center + 2],
fill=(255, 255, 255, 180),
)
return img
_SHAPE_DRAWERS = {
"round": _draw_round,
"blade": _draw_blade,
"axe": _draw_axe,
"pickaxe": _draw_pickaxe,
"crystal": _draw_crystal,
"ingot": _draw_ingot,
"liquid": _draw_liquid,
"organic": _draw_organic,
"abstract": _draw_abstract,
}
# ============================================================
# Pattern overlay
# ============================================================
def _apply_pattern(img: Image.Image, pattern: str, accent: tuple) -> None:
"""Apply a pattern on top of the existing image."""
draw = ImageDraw.Draw(img)
size = img.width
if pattern == "sparkle":
for x, y in [(4, 4), (size - 6, 5), (5, size - 6)]:
draw.point((x, y), fill=(255, 255, 255, 255))
draw.point((x + 1, y), fill=(255, 255, 255, 180))
draw.point((x, y + 1), fill=(255, 255, 255, 180))
elif pattern == "glow":
for i in range(1, 3):
draw.rectangle(
[i, i, size - 1 - i, size - 1 - i],
outline=(accent[0], accent[1], accent[2], 100 - i * 30),
)
elif pattern == "cracks":
draw.line([(3, 5), (7, 9)], fill=accent, width=1)
draw.line([(7, 9), (5, 13)], fill=accent, width=1)
elif pattern == "dots":
for x in range(3, size - 3, 4):
for y in range(3, size - 3, 4):
draw.point((x, y), fill=accent)
elif pattern == "stripes":
for y in range(3, size - 3, 3):
draw.line([(2, y), (size - 3, y)], fill=accent, width=1)
# ============================================================
# Entry points
# ============================================================
def generate_texture(item: dict, output_path: Path, size: int = 16) -> None:
"""Generate a single texture based on the item's visual features.
Args:
item: Item definition containing id, type, visual, etc.
output_path: Output PNG path.
size: Texture side length, default 16.
"""
visual = _get_visual(item)
primary = _hex_to_rgb(visual["primary_color"])
accent = _hex_to_rgb(visual["accent_color"])
shape = visual["shape"]
pattern = visual["pattern"]
drawer = _SHAPE_DRAWERS.get(shape, _draw_abstract)
img = drawer(size, primary, accent)
if pattern != "none":
_apply_pattern(img, pattern, accent)
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(output_path, "PNG")
def generate_all_textures(blueprint: dict, project_dir: Path) -> None:
"""Generate textures for all items in the blueprint.
Args:
blueprint: Blueprint dictionary.
project_dir: Root directory of the generated project.
"""
mod_id = blueprint["mod_id"]
textures_dir = (
project_dir / "src" / "main" / "resources"
/ "assets" / mod_id / "textures" / "item"
)
for item in blueprint["items"]:
output_path = textures_dir / f"{item['id']}.png"
generate_texture(item, output_path)
shape = item.get("visual", {}).get("shape", "abstract")
print(f" 🎨 Generated texture: {output_path.name} ({shape})")In modsmith/blueprint/examples/, add a visual field to each example:
01_basic_item.json:
{
"id": "ruby",
"type": "basic",
"display_name_en": "Ruby",
"display_name_zh": "红宝石",
"visual": {
"shape": "crystal",
"primary_color": "#dc2626",
"accent_color": "#fca5a5",
"pattern": "sparkle"
},
"texture": "auto"
}02_food_item.json:
{
"id": "healing_apple",
"type": "food",
"display_name_en": "Healing Apple",
"display_name_zh": "治愈苹果",
"nutrition": 4,
"saturation": 0.3,
"always_edible": true,
"effects": [{"effect": "regeneration", "duration_ticks": 200, "amplifier": 0}],
"visual": {
"shape": "round",
"primary_color": "#dc2626",
"accent_color": "#86efac",
"pattern": "sparkle"
},
"texture": "auto"
}03_tool_item.json:
{
"id": "guidite_sword",
"type": "tool",
"display_name_en": "Guidite Sword",
"display_name_zh": "基迪特剑",
"tool_type": "sword",
"durability": 455,
"mining_speed": 5.0,
"attack_damage": 1.5,
"visual": {
"shape": "blade",
"primary_color": "#94a3b8",
"accent_color": "#cbd5e1",
"pattern": "none"
},
"texture": "auto"
}In modsmith/generator/project.py's generate_project(), generate_all_textures() is already called. Confirm the code exists:
from modsmith.generator.textures import generate_all_textures
def generate_project(blueprint: dict, output_dir: Path) -> None:
render_project(blueprint, output_dir)
generate_java_items(blueprint, output_dir)
generate_resources(blueprint, output_dir)
generate_all_textures(blueprint, output_dir)
print(f"✅ Project fully generated at: {output_dir}")"""Test the texture generator."""
from pathlib import Path
from modsmith.generator.textures import generate_all_textures
blueprint = {
"mod_id": "example-mod",
"items": [
{
"id": "ruby",
"type": "basic",
"visual": {
"shape": "crystal",
"primary_color": "#dc2626",
"accent_color": "#fca5a5",
"pattern": "sparkle",
},
},
{
"id": "healing_apple",
"type": "food",
"visual": {
"shape": "round",
"primary_color": "#dc2626",
"accent_color": "#86efac",
"pattern": "sparkle",
},
},
{
"id": "guidite_sword",
"type": "tool",
"visual": {
"shape": "blade",
"primary_color": "#94a3b8",
"accent_color": "#cbd5e1",
"pattern": "none",
},
},
{
"id": "quark_gluon_plasma",
"type": "basic",
"visual": {
"shape": "liquid",
"primary_color": "#a855f7",
"accent_color": "#c084fc",
"pattern": "glow",
},
},
{
"id": "mystery_dust",
"type": "basic",
# Intentionally no visual, to test the fallback
},
],
}
output_dir = Path("./test_textures_output")
generate_all_textures(blueprint, output_dir)
print(f"Textures generated at: {output_dir}")Run:
python test_textures.pyExpected result: 5 distinctly different textures are generated under test_textures_output/src/main/resources/assets/example-mod/textures/item/.
python test_generator.pyCheck the textures:
ls generated_project/src/main/resources/assets/example-mod/textures/item/Compile verification:
cd generated_project
./gradlew buildIn-game verification (optional):
./gradlew runClientEnter creative mode and check whether each item's texture is distinctive.
Restart the Web service and say in Chat:
"I want to make an apple that heals when eaten"
The 【Requirement Summary】 output by Chat only contains the item description, without visual information. Click "⚡ Summarize Requirements" → "⚡ Generate Mod", then check blueprint.json:
{
"id": "healing_apple",
"type": "food",
"display_name_zh": "治愈苹果",
"visual": {
"shape": "round",
"primary_color": "#dc2626",
"accent_color": "#86efac",
"pattern": "sparkle"
}
}The generated healing_apple.png should be a red round fruit with a green highlight and sparkle points.
Try another:
"Make a mysterious purple potion"
It should automatically infer:
"visual": {
"shape": "liquid",
"primary_color": "#a855f7",
"accent_color": "#c084fc",
"pattern": "glow"
}The user only describes the item and its effect, and the AI automatically matches a suitable texture. This is the effect this task aims to achieve.
| Problem | Cause | Solution |
|---|---|---|
| All textures are abstract | LLM did not output visual.shape
|
Check whether the System Prompt modification in Step 2 has been applied |
| Blueprint validation fails but the LLM keeps not filling in visual | Schema did not set required
|
Check the Schema modification in Step 2 |
| Compilation passes but in-game textures are blurry | Wrong size | Ensure the generated size is 16x16 |
| Shape does not match expectations | Name inference rules do not match | Add more keywords to the inference rules in the System Prompt |
| Fallback colors are unsatisfactory | MD5 hash randomness | Explicitly provide primary_color for the item |
| Want newly added shapes to take effect too | Only added the drawing function | Also need to register it in _SHAPE_DRAWERS and add it to the Schema's enum
|
- Task 1's System Prompt has been appended with the "visual inference" rule, and the LLM proactively infers
visual. - Task 2's Schema has made
visualrequired (with internalshapeandprimary_coloralso required). -
modsmith/generator/textures.pyhas been rewritten, containing 9 shape drawing functions and 6 pattern overlays. -
_get_visual()implements the fallback: uses the MD5 hash scheme whenvisualis missing. -
generate_texture()receives the full item dictionary and extracts visual features from it. -
generate_all_textures()iterates over all items in the blueprint and generates textures. -
generate_projectalready callsgenerate_all_textures. - All 3 example blueprints have been given a
visualfield. -
test_textures.pysuccessfully generates 5 textures with different shapes. - When the user only describes the item and its effect, the LLM automatically infers a suitable texture (no need to mention colors or shapes in the description).
After completing this task, your ModSmith will be able to generate distinctive item textures and automatically match them to user requirements — when the user says "healing apple," the system automatically generates a red round fruit; when the user says "mysterious purple potion," the system automatically generates a purple bottle with a glowing effect.
-
More shapes: add
bow,wand,helmet,boots,bowl, etc. - Dynamic coloring: automatically adjust colors based on item effects (e.g., potion effect → glow).
- Multi-layer composition: an item can stack multiple shapes (e.g., "gem + key").
- AI image generation: fall back to Zhipu CogView for complex items (requires a new interface; higher cost).
- User customization: allow users to upload or choose textures in the Web UI.