"""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()