Use lightweight natural tree rendering
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
newmtl None
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 1.000000 1.000000 1.000000
|
||||
Ks 0.000000 0.000000 0.000000
|
||||
d 1.000000
|
||||
illum 1
|
||||
map_Kd HazelnutBark.png
|
||||
|
||||
newmtl None_Hazelnut.png
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 1.000000 1.000000 1.000000
|
||||
Ks 0.000000 0.000000 0.000000
|
||||
d 1.000000
|
||||
illum 1
|
||||
map_Kd HazelnutLeaves.png
|
||||
map_d HazelnutLeaves.png
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
@@ -11,10 +11,12 @@
|
||||
--osm "/path/to/input.osm" \
|
||||
--geojson "/path/to/osm2streets_web_out" \
|
||||
--output "/path/to/output.blend" \
|
||||
--render "/path/to/preview.png"
|
||||
--render "/path/to/preview.png" \
|
||||
--tree-style natural
|
||||
```
|
||||
|
||||
`--geojson` 为可选参数。不提供时,道路使用简单的 OSM highway 折线,而非详细 osm2streets 几何。
|
||||
`--tree-style` 可选 `natural` 或 `procedural`。默认 `natural` 使用低面数树干和多层不规则树冠,形态比球形程序化树冠更自然,渲染也比外部 OBJ 树模型轻;`procedural` 最轻但更卡通。
|
||||
|
||||
使用 `--office-overrides` 指定一组 OSM way ID(逗号分隔),这些建筑将渲染为办公楼风格,即使其 OSM 标签为 `building=industrial`:
|
||||
|
||||
@@ -25,7 +27,7 @@
|
||||
### 说明
|
||||
|
||||
- OSM `bounds` 元素定义场景范围(排除远处的地铁等关系成员)
|
||||
- `natural=tree` 节点 → 独立树木(程序化或模型树冠)
|
||||
- `natural=tree` 节点 → 独立树木(默认低面数自然树冠)
|
||||
- `natural=tree_row` 路径 → 沿路径均匀分布的树木
|
||||
- `landuse=grass` → 绿色地面
|
||||
- `natural=scrub` → 低矮灌木丛
|
||||
@@ -63,4 +65,4 @@ GLB 使用以 OSM bounds 中心为原点的局部 ENU 坐标系(X 东,Y 北
|
||||
```bash
|
||||
cd /path/to/output
|
||||
python3 -m http.server 8765
|
||||
```
|
||||
```
|
||||
|
||||
@@ -19,6 +19,9 @@ import numpy as np
|
||||
|
||||
EXPORT_TINTS = {
|
||||
"Grass": ((0.12, 0.48, 0.08), 0.72),
|
||||
"Tree Crown Dark": ((0.06, 0.22, 0.05), 0.18),
|
||||
"Tree Crown Light": ((0.16, 0.42, 0.09), 0.16),
|
||||
"Scrub Ground Cover": ((0.08, 0.28, 0.07), 0.28),
|
||||
"Office White Plaster Facade": ((0.92, 0.94, 0.92), 0.38),
|
||||
"Office White Metal Facade": ((0.92, 0.94, 0.92), 0.68),
|
||||
"Office Light Flat Roof": ((0.82, 0.86, 0.88), 0.35),
|
||||
@@ -33,12 +36,18 @@ EXPORT_METALLIC_OVERRIDES = {
|
||||
}
|
||||
|
||||
EXPORT_BASE_COLOR_OVERRIDES = {
|
||||
"Tree Crown": (0.11, 0.34, 0.075),
|
||||
"Tree Crown Dark": (0.065, 0.24, 0.055),
|
||||
"Tree Crown Light": (0.14, 0.40, 0.085),
|
||||
"Office White Plaster Facade": (0.93, 0.94, 0.91),
|
||||
"Office White Metal Facade": (0.93, 0.94, 0.91),
|
||||
"Office Light Flat Roof": (0.88, 0.90, 0.88),
|
||||
}
|
||||
|
||||
EXPORT_EMISSION_OVERRIDES = {
|
||||
"Tree Crown": ((0.04, 0.11, 0.035), 0.02),
|
||||
"Tree Crown Dark": ((0.025, 0.07, 0.02), 0.015),
|
||||
"Tree Crown Light": ((0.045, 0.12, 0.03), 0.015),
|
||||
"Office White Plaster Facade": ((0.93, 0.94, 0.91), 0.18),
|
||||
"Office White Metal Facade": ((0.93, 0.94, 0.91), 0.18),
|
||||
"Office Light Flat Roof": ((0.88, 0.90, 0.88), 0.14),
|
||||
@@ -85,10 +94,17 @@ def source_color(material):
|
||||
return color
|
||||
|
||||
|
||||
def source_principled_value(material, input_name, fallback):
|
||||
def principled_bsdf(material):
|
||||
if not material.use_nodes:
|
||||
return fallback
|
||||
node = material.node_tree.nodes.get("Principled BSDF")
|
||||
return None
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == "BSDF_PRINCIPLED":
|
||||
return node
|
||||
return None
|
||||
|
||||
|
||||
def source_principled_value(material, input_name, fallback):
|
||||
node = principled_bsdf(material)
|
||||
if not node or input_name not in node.inputs:
|
||||
return fallback
|
||||
return node.inputs[input_name].default_value
|
||||
@@ -144,8 +160,8 @@ def tree_crown_image():
|
||||
np.sin((x * 89.0 + y * 67.0) * np.pi) * 0.07
|
||||
)
|
||||
noise = np.clip(0.5 + noise, 0.0, 1.0)[..., None]
|
||||
dark = np.asarray((0.035, 0.16, 0.045), dtype=np.float32)
|
||||
light = np.asarray((0.12, 0.42, 0.13), dtype=np.float32)
|
||||
dark = np.asarray((0.04, 0.17, 0.04), dtype=np.float32)
|
||||
light = np.asarray((0.17, 0.46, 0.115), dtype=np.float32)
|
||||
rgb = dark + (light - dark) * noise
|
||||
rgba = np.concatenate(
|
||||
(rgb, np.ones((size, size, 1), dtype=np.float32)), axis=2)
|
||||
@@ -353,4 +369,4 @@ def export(args):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export(cli_args())
|
||||
export(cli_args())
|
||||
|
||||
@@ -27,23 +27,17 @@ import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
TEXTURE_ROOT = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), "..", "assets", "textures", "polyhaven"
|
||||
))
|
||||
MODEL_ROOT = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), "..", "assets", "models", "polyhaven"
|
||||
))
|
||||
TREE_MODEL_PATH = os.path.join(
|
||||
MODEL_ROOT, "78-hazelnutbush", "Hazelnut.obj"
|
||||
)
|
||||
|
||||
|
||||
def cli_args():
|
||||
values = {"osm": None, "geojson": None, "output": None, "render": None,
|
||||
"office_overrides": ""}
|
||||
"office_overrides": "", "tree_style": "natural"}
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
@@ -67,6 +61,8 @@ def cli_args():
|
||||
values["office_overrides"] = set()
|
||||
else:
|
||||
values["office_overrides"] = set()
|
||||
if values.get("tree_style") not in {"natural", "procedural"}:
|
||||
raise RuntimeError("--tree-style must be 'natural' or 'procedural'")
|
||||
return values
|
||||
|
||||
|
||||
@@ -145,11 +141,20 @@ def new_collection(name):
|
||||
return collection
|
||||
|
||||
|
||||
def principled_bsdf(material):
|
||||
if not material.use_nodes:
|
||||
return None
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == "BSDF_PRINCIPLED":
|
||||
return node
|
||||
return None
|
||||
|
||||
|
||||
def make_material(name, color, roughness=0.8, metallic=0.0):
|
||||
material = bpy.data.materials.get(name) or bpy.data.materials.new(name)
|
||||
material.diffuse_color = (*color, 1.0)
|
||||
material.use_nodes = True
|
||||
bsdf = material.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf = principled_bsdf(material)
|
||||
if bsdf:
|
||||
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = roughness
|
||||
@@ -160,7 +165,7 @@ def make_material(name, color, roughness=0.8, metallic=0.0):
|
||||
def add_procedural_surface(material, colors, scale=2.0, detail=2.0, bump_strength=0.08):
|
||||
nodes = material.node_tree.nodes
|
||||
links = material.node_tree.links
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
bsdf = principled_bsdf(material)
|
||||
if not bsdf:
|
||||
return
|
||||
noise = nodes.new("ShaderNodeTexNoise")
|
||||
@@ -192,7 +197,9 @@ def make_textured_material(name, diffuse_file, normal_file, roughness,
|
||||
material = make_material(name, (0.5, 0.5, 0.5), roughness, metallic)
|
||||
nodes = material.node_tree.nodes
|
||||
links = material.node_tree.links
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
bsdf = principled_bsdf(material)
|
||||
if not bsdf:
|
||||
return material
|
||||
texcoord = nodes.new("ShaderNodeTexCoord")
|
||||
mapping = nodes.new("ShaderNodeMapping")
|
||||
mapping.inputs["Scale"].default_value = (scale, scale, scale)
|
||||
@@ -231,124 +238,6 @@ def make_textured_material(name, diffuse_file, normal_file, roughness,
|
||||
return material
|
||||
|
||||
|
||||
def make_image_material(name, image_path, roughness=0.8, metallic=0.0,
|
||||
alpha_path=None, alpha_clip=0.33,
|
||||
cull_backface=False, tint=None, tint_factor=0.0,
|
||||
saturation=1.0, value=1.0, emission_color=None,
|
||||
emission_strength=0.0):
|
||||
material = make_material(name, (0.5, 0.5, 0.5), roughness, metallic)
|
||||
nodes = material.node_tree.nodes
|
||||
links = material.node_tree.links
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
if not bsdf or not os.path.exists(image_path):
|
||||
return material
|
||||
|
||||
image = nodes.new("ShaderNodeTexImage")
|
||||
image.image = bpy.data.images.load(image_path, check_existing=True)
|
||||
color_output = image.outputs["Color"]
|
||||
|
||||
if saturation != 1.0 or value != 1.0:
|
||||
hsv = nodes.new("ShaderNodeHueSaturation")
|
||||
hsv.inputs["Saturation"].default_value = saturation
|
||||
hsv.inputs["Value"].default_value = value
|
||||
links.new(color_output, hsv.inputs["Color"])
|
||||
color_output = hsv.outputs["Color"]
|
||||
|
||||
if tint and tint_factor > 0.0:
|
||||
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 = tint_factor
|
||||
links.new(color_output, mix.inputs[1])
|
||||
links.new(tint_node.outputs["Color"], mix.inputs[2])
|
||||
color_output = mix.outputs["Color"]
|
||||
|
||||
links.new(color_output, bsdf.inputs["Base Color"])
|
||||
if "Specular IOR Level" in bsdf.inputs:
|
||||
bsdf.inputs["Specular IOR Level"].default_value = 0.2
|
||||
elif "Specular" in bsdf.inputs:
|
||||
bsdf.inputs["Specular"].default_value = 0.2
|
||||
|
||||
if emission_color and emission_strength > 0.0:
|
||||
if "Emission Color" in bsdf.inputs:
|
||||
bsdf.inputs["Emission Color"].default_value = (*emission_color, 1.0)
|
||||
elif "Emission" in bsdf.inputs:
|
||||
bsdf.inputs["Emission"].default_value = (*emission_color, 1.0)
|
||||
if "Emission Strength" in bsdf.inputs:
|
||||
bsdf.inputs["Emission Strength"].default_value = emission_strength
|
||||
|
||||
alpha_source = image
|
||||
if alpha_path and os.path.exists(alpha_path):
|
||||
alpha_source = nodes.new("ShaderNodeTexImage")
|
||||
alpha_source.image = bpy.data.images.load(alpha_path, check_existing=True)
|
||||
alpha_source.image.colorspace_settings.name = "Non-Color"
|
||||
|
||||
if "Alpha" in bsdf.inputs:
|
||||
links.new(alpha_source.outputs["Alpha"], bsdf.inputs["Alpha"])
|
||||
|
||||
if hasattr(material, "blend_method"):
|
||||
material.blend_method = "CLIP" if alpha_path else "OPAQUE"
|
||||
if hasattr(material, "shadow_method"):
|
||||
material.shadow_method = "CLIP" if alpha_path else "OPAQUE"
|
||||
if hasattr(material, "alpha_threshold"):
|
||||
material.alpha_threshold = alpha_clip
|
||||
if hasattr(material, "surface_render_method") and alpha_path:
|
||||
material.surface_render_method = "DITHERED"
|
||||
if hasattr(material, "use_backface_culling"):
|
||||
material.use_backface_culling = cull_backface
|
||||
return material
|
||||
|
||||
|
||||
def import_model(filepath):
|
||||
extension = os.path.splitext(filepath)[1].lower()
|
||||
if extension in {".gltf", ".glb"}:
|
||||
bpy.ops.import_scene.gltf(filepath=filepath)
|
||||
return
|
||||
if extension == ".obj":
|
||||
if hasattr(bpy.ops.wm, "obj_import"):
|
||||
bpy.ops.wm.obj_import(filepath=filepath)
|
||||
return
|
||||
bpy.ops.import_scene.obj(filepath=filepath)
|
||||
return
|
||||
raise RuntimeError("Unsupported tree model format: " + extension)
|
||||
|
||||
|
||||
def assign_tree_model_materials(objects):
|
||||
model_dir = os.path.dirname(TREE_MODEL_PATH)
|
||||
bark = make_image_material(
|
||||
"Hazelnut Bark",
|
||||
os.path.join(model_dir, "HazelnutBark.png"),
|
||||
roughness=0.86,
|
||||
)
|
||||
leaves = make_image_material(
|
||||
"Hazelnut Leaves",
|
||||
os.path.join(model_dir, "HazelnutLeaves.png"),
|
||||
roughness=0.82,
|
||||
alpha_path=os.path.join(model_dir, "HazelnutLeavesMask.png"),
|
||||
alpha_clip=0.18,
|
||||
cull_backface=False,
|
||||
tint=(0.25, 0.52, 0.18),
|
||||
tint_factor=0.62,
|
||||
saturation=1.45,
|
||||
value=1.42,
|
||||
emission_color=(0.21, 0.36, 0.15),
|
||||
emission_strength=0.26,
|
||||
)
|
||||
for obj in objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
obj.data.materials.clear()
|
||||
lowered = obj.name.lower()
|
||||
if "leaf" in lowered:
|
||||
obj.data.materials.append(leaves)
|
||||
else:
|
||||
obj.data.materials.append(bark)
|
||||
for polygon in obj.data.polygons:
|
||||
polygon.material_index = 0
|
||||
polygon.use_smooth = True
|
||||
|
||||
|
||||
class MeshBatch:
|
||||
def __init__(self, name, collection, material):
|
||||
self.name = name
|
||||
@@ -637,66 +526,103 @@ def add_tree_batch(positions, collection, trunk_material, leaf_material):
|
||||
leaves.finish()
|
||||
|
||||
|
||||
def add_tree_model_instances(positions, collection):
|
||||
if not positions or not os.path.exists(TREE_MODEL_PATH):
|
||||
return False
|
||||
def add_natural_tree_instances(positions, collection, trunk_material,
|
||||
leaf_dark_material, leaf_light_material):
|
||||
trunk = MeshBatch("Tree_Natural_Trunks", collection, trunk_material)
|
||||
lower = MeshBatch("Tree_Natural_Crowns_Dark", collection, leaf_dark_material)
|
||||
upper = MeshBatch("Tree_Natural_Crowns_Light", collection, leaf_light_material)
|
||||
trunk_sides = 9
|
||||
crown_sides = 9
|
||||
crown_rings = 5
|
||||
|
||||
before = set(bpy.data.objects)
|
||||
try:
|
||||
import_model(TREE_MODEL_PATH)
|
||||
except Exception as exc:
|
||||
print("TREE_MODEL_IMPORT_FAILED", exc)
|
||||
return False
|
||||
|
||||
template_objects = [obj for obj in bpy.data.objects if obj not in before]
|
||||
template_meshes = [obj for obj in template_objects if obj.type == "MESH"]
|
||||
if not template_meshes:
|
||||
for obj in template_objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
return False
|
||||
|
||||
assign_tree_model_materials(template_meshes)
|
||||
for obj in template_objects:
|
||||
link_object_to_collection(obj, collection)
|
||||
|
||||
min_z = min(
|
||||
(obj.matrix_world @ Vector(corner)).z
|
||||
for obj in template_meshes
|
||||
for corner in obj.bound_box
|
||||
)
|
||||
max_z = max(
|
||||
(obj.matrix_world @ Vector(corner)).z
|
||||
for obj in template_meshes
|
||||
for corner in obj.bound_box
|
||||
)
|
||||
source_height = max(0.1, max_z - min_z)
|
||||
|
||||
base_shift = Matrix.Translation((0.0, 0.0, -min_z))
|
||||
for obj in template_objects:
|
||||
obj.matrix_world = base_shift @ obj.matrix_world
|
||||
obj.hide_viewport = True
|
||||
obj.hide_render = True
|
||||
obj.name = "Tree_Template_" + obj.name
|
||||
def add_blob(batch, cx, cy, cz, rx, ry, rz, phase, squash=1.0):
|
||||
start = len(batch.vertices)
|
||||
for ring in range(crown_rings):
|
||||
latitude = -math.pi / 2 + math.pi * ring / (crown_rings - 1)
|
||||
ring_radius = math.cos(latitude)
|
||||
for side in range(crown_sides):
|
||||
angle = math.tau * side / crown_sides
|
||||
wobble = (
|
||||
1.0 +
|
||||
0.14 * math.sin(phase + side * 1.31 + ring * 0.83) +
|
||||
0.07 * math.sin(phase * 0.7 + side * 2.11)
|
||||
)
|
||||
batch.vertices.append((
|
||||
cx + rx * ring_radius * math.cos(angle) * wobble,
|
||||
cy + ry * ring_radius * math.sin(angle) * wobble,
|
||||
cz + rz * math.sin(latitude) * squash,
|
||||
))
|
||||
for ring in range(crown_rings - 1):
|
||||
for side in range(crown_sides):
|
||||
next_side = (side + 1) % crown_sides
|
||||
batch.faces.append((
|
||||
start + ring * crown_sides + side,
|
||||
start + ring * crown_sides + next_side,
|
||||
start + (ring + 1) * crown_sides + next_side,
|
||||
start + (ring + 1) * crown_sides + side,
|
||||
))
|
||||
|
||||
for index, (x, y, height) in enumerate(positions):
|
||||
target_height = max(4.6, min(9.2, height * 1.12))
|
||||
scale = target_height / source_height
|
||||
yaw = Matrix.Rotation((index * 1.61803398875) % math.tau, 4, "Z")
|
||||
transform = Matrix.Translation((x, y, 0.0)) @ yaw @ Matrix.Diagonal(
|
||||
(scale, scale, scale, 1.0)
|
||||
)
|
||||
for template in template_objects:
|
||||
inst = template.copy()
|
||||
if template.data:
|
||||
inst.data = template.data
|
||||
inst.animation_data_clear()
|
||||
inst.matrix_world = transform @ template.matrix_world
|
||||
inst.hide_viewport = False
|
||||
inst.hide_render = False
|
||||
inst.name = "Tree_Model_" + str(index)
|
||||
collection.objects.link(inst)
|
||||
target_height = max(4.8, min(8.8, height * 1.08))
|
||||
phase = index * 1.61803398875
|
||||
trunk_height = target_height * (0.48 + 0.05 * math.sin(phase))
|
||||
trunk_radius = max(0.13, target_height * 0.038)
|
||||
lean_x = math.sin(phase * 1.7) * target_height * 0.025
|
||||
lean_y = math.cos(phase * 1.3) * target_height * 0.025
|
||||
|
||||
return True
|
||||
base = len(trunk.vertices)
|
||||
trunk_levels = [
|
||||
(0.0, trunk_radius),
|
||||
(trunk_height * 0.55, trunk_radius * 0.78),
|
||||
(trunk_height, trunk_radius * 0.48),
|
||||
]
|
||||
for level_index, (z, radius) in enumerate(trunk_levels):
|
||||
offset_x = lean_x * level_index / (len(trunk_levels) - 1)
|
||||
offset_y = lean_y * level_index / (len(trunk_levels) - 1)
|
||||
for side in range(trunk_sides):
|
||||
angle = math.tau * side / trunk_sides
|
||||
trunk.vertices.append((
|
||||
x + offset_x + radius * math.cos(angle),
|
||||
y + offset_y + radius * math.sin(angle),
|
||||
z,
|
||||
))
|
||||
trunk.faces.append(tuple(base + i for i in range(trunk_sides - 1, -1, -1)))
|
||||
for level_index in range(len(trunk_levels) - 1):
|
||||
row = base + level_index * trunk_sides
|
||||
next_row = row + trunk_sides
|
||||
for side in range(trunk_sides):
|
||||
next_side = (side + 1) % trunk_sides
|
||||
trunk.faces.append((row + side, row + next_side,
|
||||
next_row + next_side, next_row + side))
|
||||
top_row = base + (len(trunk_levels) - 1) * trunk_sides
|
||||
trunk.faces.append(tuple(top_row + i for i in range(trunk_sides)))
|
||||
|
||||
crown_x = x + lean_x
|
||||
crown_y = y + lean_y
|
||||
crown_z = trunk_height + target_height * 0.22
|
||||
crown_r = target_height * (0.35 + 0.035 * math.sin(phase * 0.9))
|
||||
|
||||
# Dark lower mass gives the canopy volume when viewed obliquely.
|
||||
add_blob(lower, crown_x, crown_y, crown_z - crown_r * 0.08,
|
||||
crown_r * 0.95, crown_r * 0.78, crown_r * 0.52,
|
||||
phase, squash=0.82)
|
||||
add_blob(lower, crown_x - crown_r * 0.46, crown_y + crown_r * 0.05,
|
||||
crown_z - crown_r * 0.02, crown_r * 0.62, crown_r * 0.50,
|
||||
crown_r * 0.42, phase + 0.8, squash=0.80)
|
||||
add_blob(lower, crown_x + crown_r * 0.42, crown_y - crown_r * 0.08,
|
||||
crown_z, crown_r * 0.58, crown_r * 0.48,
|
||||
crown_r * 0.40, phase + 1.9, squash=0.80)
|
||||
|
||||
# Lighter upper clumps break the silhouette without adding heavy geometry.
|
||||
add_blob(upper, crown_x + crown_r * 0.05, crown_y + crown_r * 0.04,
|
||||
crown_z + crown_r * 0.34, crown_r * 0.70,
|
||||
crown_r * 0.58, crown_r * 0.38, phase + 2.7, squash=0.74)
|
||||
add_blob(upper, crown_x - crown_r * 0.24, crown_y - crown_r * 0.22,
|
||||
crown_z + crown_r * 0.23, crown_r * 0.46,
|
||||
crown_r * 0.40, crown_r * 0.30, phase + 3.5, squash=0.72)
|
||||
trunk.finish()
|
||||
lower.finish()
|
||||
upper.finish()
|
||||
|
||||
|
||||
def polygon_area(ring):
|
||||
@@ -741,7 +667,7 @@ def add_scrub_patch(name, ring, material, collection):
|
||||
width = max(0.1, xmax - xmin)
|
||||
depth = max(0.1, ymax - ymin)
|
||||
area = polygon_area(ring)
|
||||
clump_count = max(8, min(70, int(area / 24.0) + 6))
|
||||
clump_count = max(10, min(90, int(area / 18.0) + 8))
|
||||
sides = 10
|
||||
rings = 4
|
||||
|
||||
@@ -780,8 +706,8 @@ def add_scrub_patch(name, ring, material, collection):
|
||||
if not point_in_polygon((x, y), ring):
|
||||
continue
|
||||
scale = 0.65 + 0.55 * ((attempts * 0.754877666) % 1.0)
|
||||
add_dome(x, y, 0.82 * scale, 0.64 * scale,
|
||||
0.18 + 0.14 * scale, attempts * 0.91)
|
||||
add_dome(x, y, 0.95 * scale, 0.72 * scale,
|
||||
0.34 + 0.28 * scale, attempts * 0.91)
|
||||
added += 1
|
||||
|
||||
obj = batch.finish()
|
||||
@@ -905,7 +831,7 @@ def build(args):
|
||||
scrub_mat = make_textured_material(
|
||||
"Scrub Ground Cover", "leafy_grass_diff_1k.jpg",
|
||||
"leafy_grass_nor_gl_1k.jpg", roughness=0.96, scale=13.0,
|
||||
tint=(0.05, 0.34, 0.08), tint_factor=0.42)
|
||||
tint=(0.09, 0.32, 0.07), tint_factor=0.42)
|
||||
fountain_mats = {
|
||||
"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),
|
||||
@@ -1068,13 +994,28 @@ def build(args):
|
||||
trees.extend(row_samples)
|
||||
row_tree_count += len(row_samples)
|
||||
if trees:
|
||||
if not add_tree_model_instances(trees, props_c):
|
||||
tree_style = args.get("tree_style")
|
||||
if tree_style == "natural":
|
||||
tree_trunk = make_textured_material(
|
||||
"Tree Trunk", "bark_brown_01_diff_1k.jpg",
|
||||
"bark_brown_01_nor_gl_1k.jpg", roughness=0.92, scale=5.0)
|
||||
tree_leaf = make_material("Tree Crown", (0.08, 0.30, 0.09), 0.88)
|
||||
leaf_dark = make_material("Tree Crown Dark", (0.065, 0.25, 0.055), 0.90)
|
||||
add_procedural_surface(leaf_dark,
|
||||
((0.035, 0.14, 0.035), (0.12, 0.36, 0.08)),
|
||||
scale=3.2, detail=3.8, bump_strength=0.08)
|
||||
leaf_light = make_material("Tree Crown Light", (0.13, 0.42, 0.09), 0.88)
|
||||
add_procedural_surface(leaf_light,
|
||||
((0.07, 0.25, 0.05), (0.22, 0.56, 0.13)),
|
||||
scale=3.6, detail=3.4, bump_strength=0.07)
|
||||
add_natural_tree_instances(trees, props_c, tree_trunk,
|
||||
leaf_dark, leaf_light)
|
||||
else:
|
||||
tree_trunk = make_textured_material(
|
||||
"Tree Trunk", "bark_brown_01_diff_1k.jpg",
|
||||
"bark_brown_01_nor_gl_1k.jpg", roughness=0.92, scale=5.0)
|
||||
tree_leaf = make_material("Tree Crown", (0.10, 0.36, 0.08), 0.88)
|
||||
add_procedural_surface(tree_leaf,
|
||||
((0.035, 0.16, 0.045), (0.12, 0.42, 0.13)),
|
||||
((0.04, 0.18, 0.04), (0.18, 0.50, 0.12)),
|
||||
scale=2.8, detail=3.2, bump_strength=0.10)
|
||||
add_tree_batch(trees, props_c, tree_trunk, tree_leaf)
|
||||
|
||||
@@ -1165,4 +1106,4 @@ def build(args):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build(cli_args())
|
||||
build(cli_args())
|
||||
|
||||
Reference in New Issue
Block a user