feat: 草坪新增一些草

This commit is contained in:
2026-07-29 14:46:29 +08:00
parent 59628cba82
commit 23ae63bc2a
7 changed files with 670 additions and 61 deletions

Binary file not shown.

View File

@@ -0,0 +1,437 @@
{
"asset": {
"generator": "Khronos glTF Blender I/O v3.5.30",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"name": "Scene",
"nodes": [
0,
1,
2,
3
]
}
],
"nodes": [
{
"mesh": 0,
"name": "shrub_02_a",
"rotation": [
0.12589912116527557,
-0.6947680711746216,
0.006097283214330673,
0.708102822303772
],
"translation": [
-1.5478131771087646,
-0.005240630358457565,
-0.02073405683040619
]
},
{
"mesh": 1,
"name": "shrub_02_b",
"rotation": [
0.08358937501907349,
-0.746630847454071,
0.26227879524230957,
0.6056113243103027
],
"translation": [
0.0007123738760128617,
-0.006340932101011276,
-0.01680799387395382
]
},
{
"mesh": 2,
"name": "shrub_02_c",
"rotation": [
0.12648986279964447,
-0.7129753232002258,
0.3943961560726166,
0.5657898783683777
],
"translation": [
1.5559492111206055,
-0.005484403111040592,
-0.006101916544139385
]
},
{
"mesh": 3,
"name": "shrub_02_d",
"rotation": [
0.047377295792102814,
-0.7383221983909607,
-0.300414502620697,
0.60198575258255
],
"translation": [
3.258664131164551,
-0.004767145495861769,
-0.005447957664728165
]
}
],
"materials": [
{
"alphaCutoff": 0.5,
"alphaMode": "MASK",
"doubleSided": true,
"name": "shrub_02",
"normalTexture": {
"index": 0
},
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 1
},
"metallicFactor": 0,
"metallicRoughnessTexture": {
"index": 2
}
}
}
],
"meshes": [
{
"name": "Plane.024",
"primitives": [
{
"attributes": {
"POSITION": 0,
"TEXCOORD_0": 1,
"NORMAL": 2
},
"indices": 3,
"material": 0
}
]
},
{
"name": "Plane.029",
"primitives": [
{
"attributes": {
"POSITION": 4,
"TEXCOORD_0": 5,
"NORMAL": 6
},
"indices": 7,
"material": 0
}
]
},
{
"name": "Plane.041",
"primitives": [
{
"attributes": {
"POSITION": 8,
"TEXCOORD_0": 9,
"NORMAL": 10
},
"indices": 11,
"material": 0
}
]
},
{
"name": "Plane.036",
"primitives": [
{
"attributes": {
"POSITION": 12,
"TEXCOORD_0": 13,
"NORMAL": 14
},
"indices": 15,
"material": 0
}
]
}
],
"textures": [
{
"sampler": 0,
"source": 0
},
{
"sampler": 0,
"source": 1
},
{
"sampler": 0,
"source": 2
}
],
"images": [
{
"mimeType": "image/jpeg",
"name": "shrub_02_nor_gl",
"uri": "textures/shrub_02_nor_gl_1k.jpg"
},
{
"mimeType": "image/jpeg",
"name": "shrub_02_diff",
"uri": "textures/shrub_02_diff_1k.jpg"
},
{
"mimeType": "image/jpeg",
"name": "shrub_02_rough",
"uri": "textures/shrub_02_arm_1k.jpg"
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 5125,
"max": [
0.6156559586524963,
1.6821810007095337,
0.42684367299079895
],
"min": [
-0.9439988136291504,
-0.03012576885521412,
-0.863244354724884
],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 5125,
"type": "VEC2"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 5125,
"type": "VEC3"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 22770,
"type": "SCALAR"
},
{
"bufferView": 4,
"componentType": 5126,
"count": 3600,
"max": [
0.9647624492645264,
1.0844767093658447,
0.30848613381385803
],
"min": [
-0.4038015305995941,
-0.10226154327392578,
-1.0773800611495972
],
"type": "VEC3"
},
{
"bufferView": 5,
"componentType": 5126,
"count": 3600,
"type": "VEC2"
},
{
"bufferView": 6,
"componentType": 5126,
"count": 3600,
"type": "VEC3"
},
{
"bufferView": 7,
"componentType": 5123,
"count": 15726,
"type": "SCALAR"
},
{
"bufferView": 8,
"componentType": 5126,
"count": 6346,
"max": [
1.287977695465088,
0.8719967007637024,
0.24143657088279724
],
"min": [
-0.8910231590270996,
-0.2947101294994354,
-1.22649085521698
],
"type": "VEC3"
},
{
"bufferView": 9,
"componentType": 5126,
"count": 6346,
"type": "VEC2"
},
{
"bufferView": 10,
"componentType": 5126,
"count": 6346,
"type": "VEC3"
},
{
"bufferView": 11,
"componentType": 5123,
"count": 27702,
"type": "SCALAR"
},
{
"bufferView": 12,
"componentType": 5126,
"count": 3528,
"max": [
0.22877177596092224,
1.153072476387024,
0.5660104751586914
],
"min": [
-1.051486849784851,
-0.09016040712594986,
-0.41910019516944885
],
"type": "VEC3"
},
{
"bufferView": 13,
"componentType": 5126,
"count": 3528,
"type": "VEC2"
},
{
"bufferView": 14,
"componentType": 5126,
"count": 3528,
"type": "VEC3"
},
{
"bufferView": 15,
"componentType": 5123,
"count": 15564,
"type": "SCALAR"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 61500,
"byteOffset": 0,
"target": 34962
},
{
"buffer": 0,
"byteLength": 41000,
"byteOffset": 61500,
"target": 34962
},
{
"buffer": 0,
"byteLength": 61500,
"byteOffset": 102500,
"target": 34962
},
{
"buffer": 0,
"byteLength": 45540,
"byteOffset": 164000,
"target": 34963
},
{
"buffer": 0,
"byteLength": 43200,
"byteOffset": 209540,
"target": 34962
},
{
"buffer": 0,
"byteLength": 28800,
"byteOffset": 252740,
"target": 34962
},
{
"buffer": 0,
"byteLength": 43200,
"byteOffset": 281540,
"target": 34962
},
{
"buffer": 0,
"byteLength": 31452,
"byteOffset": 324740,
"target": 34963
},
{
"buffer": 0,
"byteLength": 76152,
"byteOffset": 356192,
"target": 34962
},
{
"buffer": 0,
"byteLength": 50768,
"byteOffset": 432344,
"target": 34962
},
{
"buffer": 0,
"byteLength": 76152,
"byteOffset": 483112,
"target": 34962
},
{
"buffer": 0,
"byteLength": 55404,
"byteOffset": 559264,
"target": 34963
},
{
"buffer": 0,
"byteLength": 42336,
"byteOffset": 614668,
"target": 34962
},
{
"buffer": 0,
"byteLength": 28224,
"byteOffset": 657004,
"target": 34962
},
{
"buffer": 0,
"byteLength": 42336,
"byteOffset": 685228,
"target": 34962
},
{
"buffer": 0,
"byteLength": 31128,
"byteOffset": 727564,
"target": 34963
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987
}
],
"buffers": [
{
"byteLength": 758692,
"uri": "shrub_02.bin"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

View File

@@ -250,6 +250,11 @@ def make_export_material(material):
def unwrap_mesh(obj): def unwrap_mesh(obj):
if obj.type != "MESH" or not obj.data.polygons: if obj.type != "MESH" or not obj.data.polygons:
return return
# Imported assets ship authored UVs that map onto their own texture atlas;
# smart_project would scramble the leaves. Only the procedurally built
# meshes arrive without a UV layer, so that is the reliable discriminator.
if obj.data.uv_layers:
return
bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True) obj.select_set(True)
bpy.context.view_layer.objects.active = obj bpy.context.view_layer.objects.active = obj
@@ -281,6 +286,7 @@ def export(args):
material_map = {} material_map = {}
meshes = [] meshes = []
unwrapped = set()
for obj in bpy.context.scene.objects: for obj in bpy.context.scene.objects:
if obj.type != "MESH": if obj.type != "MESH":
continue continue
@@ -290,7 +296,11 @@ def export(args):
continue continue
meshes.append(obj) meshes.append(obj)
apply_mesh_modifiers(obj) apply_mesh_modifiers(obj)
unwrap_mesh(obj) # Hundreds of grass tufts share four mesh datablocks; unwrapping is a
# property of the mesh, so doing it once per datablock is enough.
if obj.data.name not in unwrapped:
unwrapped.add(obj.data.name)
unwrap_mesh(obj)
for slot in obj.material_slots: for slot in obj.material_slots:
if not slot.material: if not slot.material:
continue continue

View File

@@ -34,6 +34,31 @@ TEXTURE_ROOT = os.path.abspath(os.path.join(
os.path.dirname(__file__), "..", "assets", "textures", "polyhaven" os.path.dirname(__file__), "..", "assets", "textures", "polyhaven"
)) ))
MODEL_ROOT = os.path.abspath(os.path.join(
os.path.dirname(__file__), "..", "assets", "models", "polyhaven"
))
# Vendored under the name shrub_02, but the plant reads as a tufted grass, not
# as a bush: it belongs on the lawns. Scrub beds stay flat ground cover until a
# genuinely bush-shaped asset is sourced.
TUFT_MODEL = os.path.join(MODEL_ROOT, "shrub_02", "shrub_02_1k.gltf")
# Poly Haven ships it at ~27k triangles across four variants. The leaves are
# modelled as real geometry, so decimation eats them: at ~2.2k per variant the
# tufts render as bare twigs. Instancing makes the full mesh affordable anyway —
# every tuft shares one of four datablocks, so the scene and the exported GLB
# carry that geometry once no matter how many are scattered. 0 disables it.
TUFT_TARGET_TRIS = 0
# The variants stand 1.17-1.68m tall natively, which is shrub height. Roughly a
# third of that lands in the 0.3-0.8m range real lawn tufts occupy.
TUFT_SCALE_RANGE = (0.26, 0.46)
TUFT_SPACING = 1.7
TUFT_LIMIT_PER_LAWN = 100
# The vendored diffuse is a grey-green leaf over brown stems. Dropped onto the
# saturated lawn as-is it reads as dead weeds, so the base colour is mixed
# toward the lawn tint, a shade brighter so the tufts still separate from it.
TUFT_TINT = (0.15, 0.52, 0.09)
TUFT_TINT_FACTOR = 0.66
def cli_args(): def cli_args():
values = {"osm": None, "geojson": None, "output": None, "render": None, values = {"osm": None, "geojson": None, "output": None, "render": None,
@@ -162,7 +187,8 @@ def make_material(name, color, roughness=0.8, metallic=0.0):
return material return material
def add_procedural_surface(material, colors, scale=2.0, detail=2.0, bump_strength=0.08): def add_procedural_surface(material, colors, scale=2.0, detail=2.0, bump_strength=0.08,
object_space=False):
nodes = material.node_tree.nodes nodes = material.node_tree.nodes
links = material.node_tree.links links = material.node_tree.links
bsdf = principled_bsdf(material) bsdf = principled_bsdf(material)
@@ -179,13 +205,47 @@ def add_procedural_surface(material, colors, scale=2.0, detail=2.0, bump_strengt
bump = nodes.new("ShaderNodeBump") bump = nodes.new("ShaderNodeBump")
bump.inputs["Strength"].default_value = bump_strength bump.inputs["Strength"].default_value = bump_strength
bump.inputs["Distance"].default_value = 0.12 bump.inputs["Distance"].default_value = 0.12
links.new(texcoord.outputs["Generated"], noise.inputs["Vector"]) # "Generated" normalises across the object bounding box, so on a mesh that
# spans the whole scene the noise stretches to tens of metres and vanishes.
# Object space keeps the scale in metres, which is what foliage needs.
source = "Object" if object_space else "Generated"
links.new(texcoord.outputs[source], noise.inputs["Vector"])
links.new(noise.outputs["Fac"], ramp.inputs["Fac"]) links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"]) links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
links.new(noise.outputs["Fac"], bump.inputs["Height"]) links.new(noise.outputs["Fac"], bump.inputs["Height"])
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"]) links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
def tint_base_color(material, tint, factor):
"""Mix an existing material's base colour toward `tint`.
Imported assets arrive with their own diffuse texture wired up. Rather than
replacing it — which throws away the leaf detail — this splices a mix node
in front of the Base Color input so the texture survives at (1 - factor).
"""
if factor <= 0.0 or not material.use_nodes:
return
bsdf = principled_bsdf(material)
if not bsdf:
return
nodes = material.node_tree.nodes
links = material.node_tree.links
base = bsdf.inputs["Base Color"]
tint_node = nodes.new("ShaderNodeRGB")
tint_node.outputs["Color"].default_value = (*tint, 1.0)
mix = nodes.new("ShaderNodeMixRGB")
mix.blend_type = "MIX"
mix.inputs["Fac"].default_value = factor
if base.is_linked:
# Capture the upstream socket before relinking; Blender drops the old
# link as soon as the input takes a new one.
links.new(base.links[0].from_socket, mix.inputs[1])
else:
mix.inputs[1].default_value = base.default_value
links.new(tint_node.outputs["Color"], mix.inputs[2])
links.new(mix.outputs["Color"], base)
def make_textured_material(name, diffuse_file, normal_file, roughness, def make_textured_material(name, diffuse_file, normal_file, roughness,
scale, normal_is_bump=False, metallic=0.0, scale, normal_is_bump=False, metallic=0.0,
tint=None, tint_factor=0.0): tint=None, tint_factor=0.0):
@@ -649,7 +709,146 @@ def point_in_polygon(point, ring):
return inside return inside
def add_scrub_patch(name, ring, material, collection): def distance_to_ring(point, ring):
px, py = point
best = float("inf")
count = len(ring)
for index in range(count):
ax, ay = ring[index]
bx, by = ring[(index + 1) % count]
dx = bx - ax
dy = by - ay
length_sq = dx * dx + dy * dy
if length_sq <= 1e-9:
distance = math.hypot(px - ax, py - ay)
else:
t = ((px - ax) * dx + (py - ay) * dy) / length_sq
t = max(0.0, min(1.0, t))
distance = math.hypot(px - (ax + t * dx), py - (ay + t * dy))
if distance < best:
best = distance
return best
def load_tuft_variants():
"""Import the vendored Poly Haven plant once and return decimated meshes.
Returns [] when the asset is missing so a clean checkout still builds; the
lawns then fall back to plain textured ground.
"""
if not os.path.exists(TUFT_MODEL):
return []
before = set(bpy.data.objects)
try:
bpy.ops.import_scene.gltf(filepath=TUFT_MODEL)
except (RuntimeError, AttributeError) as error:
print("Grass tuft import failed, lawns stay flat:", error)
return []
imported = [obj for obj in set(bpy.data.objects) - before if obj.type == "MESH"]
variants = []
tinted = set()
for obj in sorted(imported, key=lambda item: item.name):
obj.data.calc_loop_triangles()
source_tris = len(obj.data.loop_triangles)
if TUFT_TARGET_TRIS and source_tris > TUFT_TARGET_TRIS:
modifier = obj.modifiers.new("TuftDecimate", "DECIMATE")
modifier.ratio = max(0.02, TUFT_TARGET_TRIS / source_tris)
bpy.context.view_layer.update()
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = bpy.data.meshes.new_from_object(evaluated)
else:
mesh = obj.data.copy()
mesh.name = "GrassTuft_" + obj.name
mesh.calc_loop_triangles()
# Nothing is saved yet at this point, so keep the datablock alive even
# if a scene ends up with no lawn polygons to instance it into.
mesh.use_fake_user = True
for material in mesh.materials:
# The vendored textures are JPEG with no alpha channel, so hashed
# transparency only costs sorting work in Blender and Cesium.
if hasattr(material, "blend_method"):
material.blend_method = "OPAQUE"
# All four variants share one material, so guard against stacking
# the mix node — and the tint with it — four times over.
if material.name not in tinted:
tint_base_color(material, TUFT_TINT, TUFT_TINT_FACTOR)
tinted.add(material.name)
variants.append(mesh)
for obj in imported:
bpy.data.objects.remove(obj, do_unlink=True)
tris = sum(len(mesh.loop_triangles) for mesh in variants)
detail = f"decimated to ~{TUFT_TARGET_TRIS} tris each" if TUFT_TARGET_TRIS else "full detail"
print(f"Grass tuft variants loaded: {len(variants)} ({detail}, {tris} tris shared)")
return variants
def tuft_density_wave(x, y):
# A lawn wants gentle clumping, not the hard banding a shrub bed needs:
# low amplitude keeps most of the polygon planted so the bare stretches
# read as mown patches rather than as dead ground.
return (math.sin(x * 0.21 + y * 0.17)
+ 0.55 * math.sin(x * 0.44 - y * 0.29 + 1.3))
def add_grass_tufts(name, ring, variants, collection):
"""Scatter the plant variants across a lawn polygon as grass tufts."""
xmin = min(x for x, _ in ring)
xmax = max(x for x, _ in ring)
ymin = min(y for _, y in ring)
ymax = max(y for _, y in ring)
def sample(spacing):
spots = []
cols = max(1, int(math.ceil((xmax - xmin) / spacing)))
rows = max(1, int(math.ceil((ymax - ymin) / spacing)))
for i in range(cols):
for j in range(rows):
# Jitter breaks up the lattice; without it the tufts read as a
# planted grid rather than as grass.
seed = (i * 73856093) ^ (j * 19349663)
jx = ((seed * 0.61803398875) % 1.0 - 0.5) * spacing * 0.8
jy = ((seed * 0.41421356237) % 1.0 - 0.5) * spacing * 0.8
x = xmin + (i + 0.5) * spacing + jx
y = ymin + (j + 0.5) * spacing + jy
if not point_in_polygon((x, y), ring):
continue
# Keep tufts off the kerb line so they do not overhang paving.
if distance_to_ring((x, y), ring) < 0.45:
continue
if tuft_density_wave(x, y) < -0.85:
continue
spots.append((x, y, seed))
return spots
# Adapt spacing to the lawn so a large polygon does not explode the export.
spacing = TUFT_SPACING
spots = sample(spacing)
while len(spots) > TUFT_LIMIT_PER_LAWN and spacing < 12.0:
spacing *= 1.22
spots = sample(spacing)
min_scale, max_scale = TUFT_SCALE_RANGE
span = max_scale - min_scale
for index, (x, y, seed) in enumerate(spots):
mesh = variants[index % len(variants)]
obj = bpy.data.objects.new(f"{name}_Tuft_{index:03d}", mesh)
scale = min_scale + span * ((seed * 0.754877666) % 1.0)
# Sunk slightly below the lawn surface so the stems never float.
obj.location = (x, y, 0.008)
obj.rotation_euler = (
math.radians(-4.0 + 8.0 * ((seed * 0.5698403) % 1.0)),
math.radians(-4.0 + 8.0 * ((seed * 0.3317554) % 1.0)),
math.tau * ((seed * 0.61803398875) % 1.0),
)
obj.scale = (scale, scale * (0.90 + 0.20 * ((seed * 0.2236068) % 1.0)), scale)
collection.objects.link(obj)
return len(spots)
def add_scrub_patch(name, ring, ground_material, collection):
if len(ring) < 3: if len(ring) < 3:
return None return None
if ring[0] == ring[-1]: if ring[0] == ring[-1]:
@@ -657,63 +856,16 @@ def add_scrub_patch(name, ring, material, collection):
if len(ring) < 3: if len(ring) < 3:
return None return None
batch = MeshBatch(name, collection, material) ground = MeshBatch(name, collection, ground_material)
batch.add_polygon(ring, 0.055) ground.add_polygon(ring, 0.055)
obj = ground.finish()
xmin = min(x for x, _ in ring) # The vendored plant asset reads as grass, so it went to the lawns. Until a
xmax = max(x for x, _ in ring) # bush-shaped model lands, scrub beds stay flat ground cover: the previous
ymin = min(y for _, y in ring) # procedural hedge mass read as blocky colour patches, not as planting.
ymax = max(y for _, y in ring)
width = max(0.1, xmax - xmin)
depth = max(0.1, ymax - ymin)
area = polygon_area(ring)
clump_count = max(10, min(90, int(area / 18.0) + 8))
sides = 10
rings = 4
def add_dome(cx, cy, rx, ry, height, phase):
start = len(batch.vertices)
for ring_index in range(rings):
t = ring_index / (rings - 1)
z = 0.055 + height * math.sin(t * math.pi / 2)
radius_scale = math.cos(t * math.pi / 2)
for side in range(sides):
angle = math.tau * side / sides
wobble = 1.0 + 0.12 * math.sin(phase + side * 1.37 + ring_index * 0.73)
batch.vertices.append((
cx + rx * radius_scale * math.cos(angle) * wobble,
cy + ry * radius_scale * math.sin(angle) * wobble,
z,
))
for ring_index in range(rings - 1):
for side in range(sides):
next_side = (side + 1) % sides
batch.faces.append((
start + ring_index * sides + side,
start + ring_index * sides + next_side,
start + (ring_index + 1) * sides + next_side,
start + (ring_index + 1) * sides + side,
))
added = 0
attempts = 0
while added < clump_count and attempts < clump_count * 8:
attempts += 1
u = (attempts * 0.61803398875) % 1.0
v = (attempts * 0.41421356237) % 1.0
x = xmin + u * width
y = ymin + v * depth
if not point_in_polygon((x, y), ring):
continue
scale = 0.65 + 0.55 * ((attempts * 0.754877666) % 1.0)
add_dome(x, y, 0.95 * scale, 0.72 * scale,
0.34 + 0.28 * scale, attempts * 0.91)
added += 1
obj = batch.finish()
if obj: if obj:
obj["scrub_texture"] = "Poly Haven leafy_grass, scrub-tinted" obj["scrub_texture"] = "Poly Haven leafy_grass, scrub-tinted"
obj["scrub_style"] = "tileable foliage ground cover with low shrub domes" obj["scrub_style"] = "flat foliage ground cover"
return obj return obj
@@ -830,8 +982,9 @@ def build(args):
roughness=0.92, scale=7.0, tint=(0.12, 0.48, 0.08), tint_factor=0.72) roughness=0.92, scale=7.0, tint=(0.12, 0.48, 0.08), tint_factor=0.72)
scrub_mat = make_textured_material( scrub_mat = make_textured_material(
"Scrub Ground Cover", "leafy_grass_diff_1k.jpg", "Scrub Ground Cover", "leafy_grass_diff_1k.jpg",
"leafy_grass_nor_gl_1k.jpg", roughness=0.96, scale=13.0, "leafy_grass_nor_gl_1k.jpg", roughness=0.96, scale=15.0,
tint=(0.09, 0.32, 0.07), tint_factor=0.42) tint=(0.085, 0.30, 0.065), tint_factor=0.46)
tuft_variants = load_tuft_variants()
fountain_mats = { fountain_mats = {
"fountain_stone": make_material("Fountain Stone", (0.42, 0.45, 0.43), 0.72), "fountain_stone": make_material("Fountain Stone", (0.42, 0.45, 0.43), 0.72),
"fountain_water": make_material("Fountain Water", (0.03, 0.32, 0.42), 0.16, 0.05), "fountain_water": make_material("Fountain Water", (0.03, 0.32, 0.42), 0.16, 0.05),
@@ -888,6 +1041,7 @@ def build(args):
tree_rows = [] tree_rows = []
lake_count = 0 lake_count = 0
grass_count = 0 grass_count = 0
grass_tuft_count = 0
scrub_count = 0 scrub_count = 0
fountain_count = 0 fountain_count = 0
building_count = 0 building_count = 0
@@ -915,8 +1069,14 @@ def build(args):
batch = MeshBatch("Grass_" + str(way["id"]), green_c, grass_mat) batch = MeshBatch("Grass_" + str(way["id"]), green_c, grass_mat)
if len(ring) >= 3: if len(ring) >= 3:
batch.add_polygon(ring, 0.015) batch.add_polygon(ring, 0.015)
batch.finish() obj = batch.finish()
grass_count += 1 grass_count += 1
if tuft_variants:
tufts = add_grass_tufts("Grass_" + str(way["id"]), ring,
tuft_variants, green_c)
grass_tuft_count += tufts
if obj:
obj["grass_tufts"] = tufts
elif tag.get("natural") == "scrub" and len(ring) >= 3: elif tag.get("natural") == "scrub" and len(ring) >= 3:
ring = clip_polygon(ring, scene_xmin, scene_xmax, ring = clip_polygon(ring, scene_xmin, scene_xmax,
scene_ymin, scene_ymax) scene_ymin, scene_ymax)
@@ -1079,6 +1239,7 @@ def build(args):
scene["office_override_way_ids"] = json.dumps(sorted(args["office_overrides"])) scene["office_override_way_ids"] = json.dumps(sorted(args["office_overrides"]))
scene["lake_count"] = lake_count scene["lake_count"] = lake_count
scene["grass_count"] = grass_count scene["grass_count"] = grass_count
scene["grass_tuft_count"] = grass_tuft_count
scene["scrub_count"] = scrub_count scene["scrub_count"] = scrub_count
scene["fountain_count"] = fountain_count scene["fountain_count"] = fountain_count
scene["tree_node_count"] = individual_tree_count scene["tree_node_count"] = individual_tree_count
@@ -1097,6 +1258,7 @@ def build(args):
"industrial_buildings": industrial_count, "industrial_buildings": industrial_count,
"lake": lake_count, "lake": lake_count,
"grass": grass_count, "grass": grass_count,
"grass_tufts": grass_tuft_count,
"scrub": scrub_count, "scrub": scrub_count,
"fountains": fountain_count, "fountains": fountain_count,
"tree_nodes": individual_tree_count, "tree_nodes": individual_tree_count,