feat(environment): add painterly skies and living meadows

This commit is contained in:
Rijad Zuzo
2026-09-05 20:27:09 +02:00
parent 7899d36f03
commit ec16e2f2ca
71 changed files with 2280 additions and 213 deletions
+176
View File
@@ -0,0 +1,176 @@
"""Build the original Jajce meadow flowers in Blender 5.1+.
Blender --background --python tools/art/build_meadow_flowers.py
Each clump has one opaque vertex-colored surface and no textures. Coordinates
are authored Y-up and converted for Blender; GLB export restores Godot Y-up.
The .blend source files are editable and excluded from Godot import.
"""
import json
import math
from pathlib import Path
import bpy
from mathutils import Vector
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "art/meadow"
OUTPUT = ROOT / "assets/meadow"
SOURCE.mkdir(parents=True, exist_ok=True)
OUTPUT.mkdir(parents=True, exist_ok=True)
def linear(hex_color):
values = [int(hex_color[i:i + 2], 16) / 255 for i in (0, 2, 4)]
return tuple(v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in values)
def coord(point):
return (point[0], -point[2], point[1])
class FlowerMesh:
def __init__(self):
self.vertices = []
self.faces = []
self.colors = []
def face(self, points, color):
first = len(self.vertices)
self.vertices.extend(coord(point) for point in points)
self.faces.append(tuple(range(first, first + len(points))))
self.colors.extend([(*linear(color), 1.0)] * len(points))
def stem(self, base, top):
bottom = Vector(base)
tip = Vector(top)
middle = bottom.lerp(tip, 0.55) + Vector((0.015, 0.0, -0.018))
for start, end in ((bottom, middle), (middle, tip)):
for side in range(4):
a = side * math.tau / 4
b = (side + 1) * math.tau / 4
edge_a = Vector((math.cos(a), 0, math.sin(a))) * 0.009
edge_b = Vector((math.cos(b), 0, math.sin(b))) * 0.009
self.face((start + edge_a, start + edge_b, end + edge_b * 0.5, end + edge_a * 0.5), "54864b")
def leaf(self, base, angle, length=0.18, width=0.042):
root = Vector(base)
direction = Vector((math.cos(angle), 0.6, math.sin(angle))) * length
side = Vector((-math.sin(angle), 0, math.cos(angle))) * width
middle = root + direction * 0.52
tip = root + direction
# Raised crease gives a leaf silhouette from both ground and game camera.
crease = middle + Vector((0, 0.028, 0))
self.face((root, middle + side, crease), "6fa74f")
self.face((middle + side, tip, crease), "80af58")
self.face((root, crease, middle - side), "4f8548")
self.face((middle - side, crease, tip), "679c4e")
def blossom(self, center, radius, petals, color, highlight, heart="e6b344"):
center = Vector(center)
for petal in range(petals):
angle = petal * math.tau / petals + 0.12
forward = Vector((math.cos(angle), 0, math.sin(angle)))
side = Vector((-math.sin(angle), 0, math.cos(angle)))
root = center + forward * radius * 0.12
middle = center + forward * radius * 0.65 + Vector((0, radius * 0.09, 0))
tip = center + forward * radius + Vector((0, radius * 0.24, 0))
width = radius * (0.30 if petals < 7 else 0.23)
self.face((root, middle + side * width, tip + side * width * 0.42), color)
self.face((root, tip + side * width * 0.42, tip - side * width * 0.42), highlight)
self.face((root, tip - side * width * 0.42, middle - side * width), color)
# An opaque domed pollen heart rather than a texture or billboard.
for side in range(8):
a = side * math.tau / 8
b = (side + 1) * math.tau / 8
self.face((center + Vector((0, radius * 0.24, 0)),
center + Vector((math.cos(a) * radius * 0.24, 0.008, math.sin(a) * radius * 0.24)),
center + Vector((math.cos(b) * radius * 0.24, 0.008, math.sin(b) * radius * 0.24))), heart)
def build(self, name):
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(self.vertices, [], self.faces)
mesh.update()
attr = mesh.color_attributes.new(name="Color", type="FLOAT_COLOR", domain="CORNER")
for index, loop in enumerate(mesh.loops):
attr.data[index].color = self.colors[loop.vertex_index]
mesh.materials.append(bpy.data.materials["MeadowPalette"])
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
return obj
def clear_scene():
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
def create_material():
material = bpy.data.materials.new("MeadowPalette")
material.use_nodes = True
material.use_backface_culling = False
nodes = material.node_tree.nodes
nodes.clear()
color = nodes.new("ShaderNodeVertexColor")
color.layer_name = "Color"
surface = nodes.new("ShaderNodeBsdfPrincipled")
surface.inputs["Roughness"].default_value = 1.0
material.node_tree.links.new(color.outputs["Color"], surface.inputs["Base Color"])
output = nodes.new("ShaderNodeOutputMaterial")
material.node_tree.links.new(surface.outputs["BSDF"], output.inputs["Surface"])
def flower(name, colors, petals, height, radius):
clear_scene()
builder = FlowerMesh()
for index, (x, z, scale) in enumerate(((0, 0, 1.0), (-0.16, 0.1, 0.76), (0.16, -0.1, 0.88))):
top = (x + 0.035 * math.cos(index * 2.2), height * scale, z + 0.04)
builder.stem((x, 0, z), top)
for leaf in range(3):
y = height * scale * (0.17 + leaf * 0.16)
builder.leaf((x, y, z), index * 2.1 + leaf * 2.45, 0.17 * scale, 0.034)
if name == "lavender_spire":
for tier in range(4):
pos = (top[0], top[1] - tier * 0.086, top[2])
builder.blossom(pos, radius * (0.58 + tier * 0.13), 5, *colors, heart="c7b3df")
else:
builder.blossom(top, radius * scale, petals, *colors)
obj = builder.build(name)
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.export_scene.gltf(
filepath=str(OUTPUT / (name + ".glb")), export_format="GLB",
use_selection=True, export_yup=True, export_animations=False,
export_vertex_color="ACTIVE", export_all_vertex_colors=False,
export_materials="EXPORT", export_extras=False, export_cameras=False,
)
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == "VIEW_3D":
area.spaces.active.shading.type = "MATERIAL"
bpy.context.preferences.filepaths.save_version = 0
bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE / (name + ".blend")))
obj.data.calc_loop_triangles()
return {
"triangles": len(obj.data.loop_triangles), "surfaces": 1,
"flower_stems": 3, "bytes": (OUTPUT / (name + ".glb")).stat().st_size,
}
def main():
clear_scene()
create_material()
specs = (
("cream_daisy", ("f0e7c4", "fff4d8"), 8, 0.47, 0.125),
("pink_cosmos", ("d780a7", "f1abc4"), 7, 0.64, 0.165),
("yellow_buttercup", ("edc45a", "ffdf83"), 5, 0.36, 0.10),
("lavender_spire", ("9585b8", "bfafd6"), 5, 0.79, 0.080),
)
stats = {spec[0]: flower(*spec) for spec in specs}
(OUTPUT / "mesh_budget.json").write_text(json.dumps(stats, indent=2) + "\n")
print(json.dumps(stats, indent=2))
if __name__ == "__main__":
main()