bolajiev commited on
Commit ·
e0e4588
1
Parent(s): 82b7ebe
Sprint 2: material presets via MeshPhysicalMaterial
Browse filesAdd optional `preset` field (gold/chrome/glass/neon/matte/plastic) to Obj and
ExtrudeNode. Preset routes through _preset_mat_js() in realistic style; wireframe/
toon/flat style overrides still win. gold/chrome use clearcoat for polished metal,
glass uses physical transmission+ior, neon maps node.color → emissiveIntensity:2.
Existing scenes with no preset are unchanged. Add 3 FEWSHOT examples and update
SYSTEM prompt with preset field documentation.
- compiler.py +49 -0
- llm.py +54 -1
- scene.py +19 -0
compiler.py
CHANGED
|
@@ -69,6 +69,51 @@ def geometry_js(o: Obj) -> str:
|
|
| 69 |
return "new THREE.BoxGeometry(1, 1, 1)"
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
def material_js(o: Obj, style: str = "realistic") -> str:
|
| 73 |
col = _col_str(o.color)
|
| 74 |
emi = _col_str(o.emissive)
|
|
@@ -79,6 +124,10 @@ def material_js(o: Obj, style: str = "realistic") -> str:
|
|
| 79 |
if style == "flat":
|
| 80 |
return (f"new THREE.MeshStandardMaterial({{ color: {col}, emissive: {emi}, "
|
| 81 |
f"metalness: {o.metalness}, roughness: {o.roughness}, flatShading: true }})")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
# realistic — honour per-object material field
|
| 83 |
if o.material == "basic":
|
| 84 |
return f"new THREE.MeshBasicMaterial({{ color: {col} }})"
|
|
|
|
| 69 |
return "new THREE.BoxGeometry(1, 1, 1)"
|
| 70 |
|
| 71 |
|
| 72 |
+
def _preset_mat_js(node) -> str | None:
|
| 73 |
+
"""Return a JS material string for a named preset, or None if no preset is set.
|
| 74 |
+
|
| 75 |
+
color override: if node.color differs from the default (#88ccff), use it;
|
| 76 |
+
otherwise fall back to each preset's canonical color so it looks right out of the box.
|
| 77 |
+
For neon, node.color is the emissive glow color regardless.
|
| 78 |
+
"""
|
| 79 |
+
preset = getattr(node, "preset", None)
|
| 80 |
+
if not preset:
|
| 81 |
+
return None
|
| 82 |
+
col = _col_str(node.color)
|
| 83 |
+
has_custom = node.color != "#88ccff"
|
| 84 |
+
|
| 85 |
+
if preset == "gold":
|
| 86 |
+
c = col if has_custom else _col_str("#ffd700")
|
| 87 |
+
return (f"new THREE.MeshPhysicalMaterial({{ color: {c}, "
|
| 88 |
+
f"metalness: 1.0, roughness: 0.25, clearcoat: 1.0, clearcoatRoughness: 0.1 }})")
|
| 89 |
+
|
| 90 |
+
if preset == "chrome":
|
| 91 |
+
c = col if has_custom else _col_str("#ffffff")
|
| 92 |
+
return (f"new THREE.MeshPhysicalMaterial({{ color: {c}, "
|
| 93 |
+
f"metalness: 1.0, roughness: 0.05, clearcoat: 1.0, clearcoatRoughness: 0.05 }})")
|
| 94 |
+
|
| 95 |
+
if preset == "glass":
|
| 96 |
+
return (f"new THREE.MeshPhysicalMaterial({{ color: {col}, "
|
| 97 |
+
f"metalness: 0.0, roughness: 0.05, "
|
| 98 |
+
f"transmission: 1.0, thickness: 0.5, ior: 1.5, "
|
| 99 |
+
f"transparent: true, opacity: 1.0 }})")
|
| 100 |
+
|
| 101 |
+
if preset == "neon":
|
| 102 |
+
return (f"new THREE.MeshStandardMaterial({{ "
|
| 103 |
+
f"color: new THREE.Color('#000000'), "
|
| 104 |
+
f"emissive: {col}, emissiveIntensity: 2.0 }})")
|
| 105 |
+
|
| 106 |
+
if preset == "matte":
|
| 107 |
+
return (f"new THREE.MeshStandardMaterial({{ color: {col}, "
|
| 108 |
+
f"metalness: 0.0, roughness: 0.9 }})")
|
| 109 |
+
|
| 110 |
+
if preset == "plastic":
|
| 111 |
+
return (f"new THREE.MeshPhysicalMaterial({{ color: {col}, "
|
| 112 |
+
f"metalness: 0.0, roughness: 0.4, clearcoat: 0.5, clearcoatRoughness: 0.3 }})")
|
| 113 |
+
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
|
| 117 |
def material_js(o: Obj, style: str = "realistic") -> str:
|
| 118 |
col = _col_str(o.color)
|
| 119 |
emi = _col_str(o.emissive)
|
|
|
|
| 124 |
if style == "flat":
|
| 125 |
return (f"new THREE.MeshStandardMaterial({{ color: {col}, emissive: {emi}, "
|
| 126 |
f"metalness: {o.metalness}, roughness: {o.roughness}, flatShading: true }})")
|
| 127 |
+
# Preset overrides per-object material field in realistic mode
|
| 128 |
+
preset_mat = _preset_mat_js(o)
|
| 129 |
+
if preset_mat:
|
| 130 |
+
return preset_mat
|
| 131 |
# realistic — honour per-object material field
|
| 132 |
if o.material == "basic":
|
| 133 |
return f"new THREE.MeshBasicMaterial({{ color: {col} }})"
|
llm.py
CHANGED
|
@@ -35,6 +35,7 @@ Schema — primitive object:
|
|
| 35 |
"scale": [x, y, z],
|
| 36 |
"color": "#RRGGBB",
|
| 37 |
"material": "standard|basic|phong|wireframe",
|
|
|
|
| 38 |
"metalness": 0.0-1.0,
|
| 39 |
"roughness": 0.0-1.0,
|
| 40 |
"emissive": "#RRGGBB",
|
|
@@ -54,13 +55,21 @@ For stars, badges, shields, hearts, hexagons, coins, logos, or emblems use an EX
|
|
| 54 |
"depth": 0.2,
|
| 55 |
"bevel": true,
|
| 56 |
"color": "#RRGGBB",
|
| 57 |
-
"
|
| 58 |
"metalness": 0.0-1.0,
|
| 59 |
"roughness": 0.0-1.0,
|
| 60 |
"emissive": "#RRGGBB",
|
| 61 |
"position": [x, y, z]
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
Keep scenes small (1-4 objects). Center the composition near the origin."""
|
| 65 |
|
| 66 |
FEWSHOT = [
|
|
@@ -211,6 +220,50 @@ FEWSHOT = [
|
|
| 211 |
],
|
| 212 |
"animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
|
| 213 |
})},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
]
|
| 215 |
|
| 216 |
_tok = None
|
|
|
|
| 35 |
"scale": [x, y, z],
|
| 36 |
"color": "#RRGGBB",
|
| 37 |
"material": "standard|basic|phong|wireframe",
|
| 38 |
+
"preset": "gold|chrome|glass|neon|matte|plastic",
|
| 39 |
"metalness": 0.0-1.0,
|
| 40 |
"roughness": 0.0-1.0,
|
| 41 |
"emissive": "#RRGGBB",
|
|
|
|
| 55 |
"depth": 0.2,
|
| 56 |
"bevel": true,
|
| 57 |
"color": "#RRGGBB",
|
| 58 |
+
"preset": "gold|chrome|glass|neon|matte|plastic",
|
| 59 |
"metalness": 0.0-1.0,
|
| 60 |
"roughness": 0.0-1.0,
|
| 61 |
"emissive": "#RRGGBB",
|
| 62 |
"position": [x, y, z]
|
| 63 |
}
|
| 64 |
|
| 65 |
+
Material preset notes:
|
| 66 |
+
- gold/chrome: use for shiny metallic looks; color overrides the base hue
|
| 67 |
+
- glass: transparent refractive; color adds a tint
|
| 68 |
+
- neon: glowing edge; set color to the desired glow color
|
| 69 |
+
- matte: no shine, soft diffuse
|
| 70 |
+
- plastic: slight sheen, clearcoat
|
| 71 |
+
Omit preset to use metalness/roughness directly.
|
| 72 |
+
|
| 73 |
Keep scenes small (1-4 objects). Center the composition near the origin."""
|
| 74 |
|
| 75 |
FEWSHOT = [
|
|
|
|
| 220 |
],
|
| 221 |
"animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
|
| 222 |
})},
|
| 223 |
+
{"role": "user", "content": "a glass star badge"},
|
| 224 |
+
{"role": "assistant", "content": json.dumps({
|
| 225 |
+
"background": "#0a0a18",
|
| 226 |
+
"objects": [{
|
| 227 |
+
"type": "extrude", "shape": "star", "depth": 0.25, "bevel": True,
|
| 228 |
+
"color": "#aaddff", "preset": "glass",
|
| 229 |
+
"position": [0, 0, 0],
|
| 230 |
+
}],
|
| 231 |
+
"lights": [
|
| 232 |
+
{"type": "ambient", "intensity": 0.6},
|
| 233 |
+
{"type": "directional", "color": "#ffffff", "intensity": 2.0, "position": [5, 8, 4]},
|
| 234 |
+
{"type": "point", "color": "#88ccff", "intensity": 1.5, "position": [-3, 2, 3]},
|
| 235 |
+
],
|
| 236 |
+
"animation": {"type": "rotate", "speed": 0.5, "axis": "y"},
|
| 237 |
+
})},
|
| 238 |
+
{"role": "user", "content": "a neon glowing shield"},
|
| 239 |
+
{"role": "assistant", "content": json.dumps({
|
| 240 |
+
"background": "#050508",
|
| 241 |
+
"objects": [{
|
| 242 |
+
"type": "extrude", "shape": "shield", "depth": 0.2, "bevel": True,
|
| 243 |
+
"color": "#ff2266", "preset": "neon",
|
| 244 |
+
"position": [0, 0, 0],
|
| 245 |
+
}],
|
| 246 |
+
"lights": [
|
| 247 |
+
{"type": "ambient", "intensity": 0.1},
|
| 248 |
+
{"type": "point", "color": "#ff2266", "intensity": 3.0, "position": [0, 1, 2]},
|
| 249 |
+
],
|
| 250 |
+
"animation": {"type": "float", "speed": 1.0, "axis": "y"},
|
| 251 |
+
})},
|
| 252 |
+
{"role": "user", "content": "a chrome hexagon"},
|
| 253 |
+
{"role": "assistant", "content": json.dumps({
|
| 254 |
+
"background": "#1a1a2e",
|
| 255 |
+
"objects": [{
|
| 256 |
+
"type": "extrude", "shape": "hexagon", "depth": 0.3, "bevel": True,
|
| 257 |
+
"color": "#ffffff", "preset": "chrome",
|
| 258 |
+
"position": [0, 0, 0],
|
| 259 |
+
}],
|
| 260 |
+
"lights": [
|
| 261 |
+
{"type": "ambient", "intensity": 0.4},
|
| 262 |
+
{"type": "directional", "color": "#ffffff", "intensity": 2.5, "position": [4, 6, 3]},
|
| 263 |
+
{"type": "point", "color": "#88aaff", "intensity": 1.0, "position": [-3, 2, 2]},
|
| 264 |
+
],
|
| 265 |
+
"animation": {"type": "rotate", "speed": 0.6, "axis": "y"},
|
| 266 |
+
})},
|
| 267 |
]
|
| 268 |
|
| 269 |
_tok = None
|
scene.py
CHANGED
|
@@ -23,6 +23,7 @@ SHAPES = {
|
|
| 23 |
}
|
| 24 |
EXTRUDE_SHAPES = {"star", "heart", "hexagon", "badge", "shield"}
|
| 25 |
MATERIALS = {"standard", "basic", "phong", "wireframe"}
|
|
|
|
| 26 |
LIGHT_TYPES = {"ambient", "directional", "point"}
|
| 27 |
ANIM_TYPES = {"none", "rotate", "float", "orbit"}
|
| 28 |
HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
@@ -141,6 +142,7 @@ class Obj(BaseModel):
|
|
| 141 |
scale: List[float] = Field(default_factory=lambda: [1.0, 1.0, 1.0])
|
| 142 |
color: str = "#88ccff"
|
| 143 |
material: str = "standard"
|
|
|
|
| 144 |
metalness: float = 0.3
|
| 145 |
roughness: float = 0.4
|
| 146 |
emissive: str = "#000000"
|
|
@@ -158,6 +160,14 @@ class Obj(BaseModel):
|
|
| 158 |
v = str(v)
|
| 159 |
return v if v in MATERIALS else "standard"
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
@field_validator("position", "rotation", "scale")
|
| 162 |
@classmethod
|
| 163 |
def _vec3(cls, v: Any, info) -> List[float]:
|
|
@@ -364,6 +374,7 @@ class ExtrudeNode(BaseModel):
|
|
| 364 |
bevel: bool = True
|
| 365 |
color: str = "#88ccff"
|
| 366 |
material: str = "standard"
|
|
|
|
| 367 |
metalness: float = 0.3
|
| 368 |
roughness: float = 0.4
|
| 369 |
emissive: str = "#000000"
|
|
@@ -397,6 +408,14 @@ class ExtrudeNode(BaseModel):
|
|
| 397 |
v = str(v)
|
| 398 |
return v if v in MATERIALS else "standard"
|
| 399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
@field_validator("metalness", "roughness")
|
| 401 |
@classmethod
|
| 402 |
def _unit(cls, v: Any) -> float:
|
|
|
|
| 23 |
}
|
| 24 |
EXTRUDE_SHAPES = {"star", "heart", "hexagon", "badge", "shield"}
|
| 25 |
MATERIALS = {"standard", "basic", "phong", "wireframe"}
|
| 26 |
+
PRESET_NAMES = {"gold", "chrome", "glass", "neon", "matte", "plastic"}
|
| 27 |
LIGHT_TYPES = {"ambient", "directional", "point"}
|
| 28 |
ANIM_TYPES = {"none", "rotate", "float", "orbit"}
|
| 29 |
HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
|
|
| 142 |
scale: List[float] = Field(default_factory=lambda: [1.0, 1.0, 1.0])
|
| 143 |
color: str = "#88ccff"
|
| 144 |
material: str = "standard"
|
| 145 |
+
preset: Optional[str] = None
|
| 146 |
metalness: float = 0.3
|
| 147 |
roughness: float = 0.4
|
| 148 |
emissive: str = "#000000"
|
|
|
|
| 160 |
v = str(v)
|
| 161 |
return v if v in MATERIALS else "standard"
|
| 162 |
|
| 163 |
+
@field_validator("preset")
|
| 164 |
+
@classmethod
|
| 165 |
+
def _preset_field(cls, v: Any) -> Optional[str]:
|
| 166 |
+
if v is None:
|
| 167 |
+
return None
|
| 168 |
+
v = str(v).lower().strip()
|
| 169 |
+
return v if v in PRESET_NAMES else None
|
| 170 |
+
|
| 171 |
@field_validator("position", "rotation", "scale")
|
| 172 |
@classmethod
|
| 173 |
def _vec3(cls, v: Any, info) -> List[float]:
|
|
|
|
| 374 |
bevel: bool = True
|
| 375 |
color: str = "#88ccff"
|
| 376 |
material: str = "standard"
|
| 377 |
+
preset: Optional[str] = None
|
| 378 |
metalness: float = 0.3
|
| 379 |
roughness: float = 0.4
|
| 380 |
emissive: str = "#000000"
|
|
|
|
| 408 |
v = str(v)
|
| 409 |
return v if v in MATERIALS else "standard"
|
| 410 |
|
| 411 |
+
@field_validator("preset")
|
| 412 |
+
@classmethod
|
| 413 |
+
def _preset_field(cls, v: Any) -> Optional[str]:
|
| 414 |
+
if v is None:
|
| 415 |
+
return None
|
| 416 |
+
v = str(v).lower().strip()
|
| 417 |
+
return v if v in PRESET_NAMES else None
|
| 418 |
+
|
| 419 |
@field_validator("metalness", "roughness")
|
| 420 |
@classmethod
|
| 421 |
def _unit(cls, v: Any) -> float:
|