Tweaking settings

This commit is contained in:
Jude Southworth committed 2026-10-27 10:09:48 +00:00
1 parent e6a0750cb9
commit a7ed716983
6 files changed
+132 -6

No files matched your search

+105 -3
View File
@@ -27,6 +27,7 @@ overrides, --light-strength, --light-left/--light-up, --ambient, --unlit,
import argparse
import math
import os
import random
import shutil
import sys
@@ -94,6 +95,12 @@ def parse_args():
help="degrees the light is raised above the camera's view axis (negative = down)")
p.add_argument("--ambient", type=float, default=0.1,
help="uniform fill light strength (0 = none)")
p.add_argument("--dew", type=int, default=0, help="number of dew drops to scatter (0 = none)")
p.add_argument("--dew-size", type=float, default=0.03,
help="largest drop radius as a fraction of the model's size")
p.add_argument("--dew-flatness", type=float, default=0.55,
help="drop height relative to its radius (1 = round bead)")
p.add_argument("--dew-seed", type=int, default=0, help="random seed for drop placement")
p.add_argument("--unlit", action="store_true",
help="skip lighting/roughness/normal; emit the blended colour as-is")
p.add_argument("--resolution", type=int, default=1024, help="square output size in px")
@@ -172,8 +179,8 @@ def uv_islands(bm, uv_layer):
return islands
def mark_stem(meshes, max_width):
"""Store a 'projection_keep' face-corner attribute (0 on the stem) and return non-stem vertices.
def find_stem_faces(meshes, max_width):
"""Polygon indices of the stem, per mesh name.
The stem is any UV island in the top half of the model whose horizontal
extent is under max_width x the model's width.
@@ -181,7 +188,7 @@ def mark_stem(meshes, max_width):
center, _, _ = bounds(meshes)
corners = [o.matrix_world @ Vector(c) for o in meshes for c in o.bound_box]
model_width = max(max(c[i] for c in corners) - min(c[i] for c in corners) for i in (0, 1))
body_verts = []
stems = {}
for obj in meshes:
mesh, mw = obj.data, obj.matrix_world
stem_faces = set()
@@ -194,6 +201,17 @@ def mark_stem(meshes, max_width):
if spread < max_width * model_width and min(p.z for p in pts) > center.z:
stem_faces.update(f.index for f in island)
bm.free()
stems[obj.name] = stem_faces
return stems
def mark_stem(meshes, max_width):
"""Store a 'projection_keep' face-corner attribute (0 on the stem) and return non-stem vertices."""
stems = find_stem_faces(meshes, max_width)
body_verts = []
for obj in meshes:
mesh, mw = obj.data, obj.matrix_world
stem_faces = stems[obj.name]
print(f"{obj.name}: {len(stem_faces)} stem faces kept as-is")
keep = mesh.attributes.get("projection_keep") or \
@@ -380,6 +398,86 @@ def make_material(args, maps, texture):
return mat
def add_dew(scene, meshes, args):
"""Scatter flattened water droplets over the skin (not the stem or the underside)."""
rng = random.Random(args.dew_seed)
_, _, extent = bounds(meshes)
stems = find_stem_faces(meshes, args.stem_max_width)
# Candidate triangles, weighted by area, so droplets are spread evenly over the surface.
tris, weights = [], []
for obj in meshes:
mesh, mw = obj.data, obj.matrix_world
normal_mw = mw.to_3x3().inverted().transposed()
mesh.calc_loop_triangles()
for tri in mesh.loop_triangles:
if tri.polygon_index in stems[obj.name]:
continue
normal = (normal_mw @ tri.normal).normalized()
if normal.z < -0.3: # underside
continue
pts = [mw @ mesh.vertices[v].co for v in tri.vertices]
tris.append((pts, normal))
weights.append((pts[1] - pts[0]).cross(pts[2] - pts[0]).length / 2)
if not tris:
return
# Clear water, darkened towards the edges so each drop's outline reads.
water = bpy.data.materials.new("water")
water.use_nodes = True
nodes, links = water.node_tree.nodes, water.node_tree.links
bsdf = nodes["Principled BSDF"]
bsdf.inputs["Base Color"].default_value = (1, 1, 1, 1)
bsdf.inputs["Roughness"].default_value = 0.02
bsdf.inputs["IOR"].default_value = 1.33
bsdf.inputs["Transmission Weight"].default_value = 1.0
rim = nodes.new("ShaderNodeBsdfGlossy")
rim.inputs["Color"].default_value = (0.05, 0.05, 0.05, 1)
rim.inputs["Roughness"].default_value = 0.02
edge = nodes.new("ShaderNodeLayerWeight")
edge.inputs["Blend"].default_value = 0.35
mix = nodes.new("ShaderNodeMixShader")
links.new(edge.outputs["Facing"], mix.inputs["Fac"])
links.new(bsdf.outputs["BSDF"], mix.inputs[1])
links.new(rim.outputs["BSDF"], mix.inputs[2])
links.new(mix.outputs["Shader"], nodes["Material Output"].inputs["Surface"])
# Light refracting through the drops (caustics) only adds fireflies here.
scene.cycles.caustics_refractive = False
scene.cycles.caustics_reflective = False
scene.cycles.blur_glossy = 1.0
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=24, v_segments=12, radius=1.0)
for face in bm.faces:
face.smooth = True
drop_mesh = bpy.data.meshes.new("dew_drop")
bm.to_mesh(drop_mesh)
bm.free()
drop_mesh.materials.append(water)
collection = bpy.data.collections.new("dew")
scene.collection.children.link(collection)
for i, (pts, normal) in enumerate(rng.choices(tris, weights, k=args.dew)):
a, b = rng.random(), rng.random()
if a + b > 1:
a, b = 1 - a, 1 - b
point = pts[0] + (pts[1] - pts[0]) * a + (pts[2] - pts[0]) * b
# Log-uniform size between dew-size/2.5 and dew-size, as a fraction of the model.
r = extent * args.dew_size * math.exp(rng.uniform(math.log(0.4), 0))
height = r * args.dew_flatness
drop = bpy.data.objects.new(f"dew_{i}", drop_mesh)
drop.scale = (r, r, height)
drop.rotation_euler = normal.to_track_quat("Z", "Y").to_euler()
# Sink the drop so roughly its top 70% sits proud of the skin.
drop.location = point + normal * height * 0.4
# Light reaching the skin through a drop is a caustic that renders dark and
# noisy; letting it pass straight through keeps the skin under the drop lit.
drop.visible_shadow = False
collection.objects.link(drop)
print(f"Added {args.dew} dew drops")
def find_maps(args, meshes):
original = next((m for o in meshes for m in o.data.materials if m), None)
maps = {}
@@ -500,6 +598,10 @@ def main():
center, dist, radius = frame_camera(scene, meshes, args.azimuth, args.elevation, args.margin)
if not args.unlit:
add_light_behind_camera(scene, center, dist, radius, args)
if args.dew:
add_dew(scene, meshes, args)
elif args.dew:
print("--dew is ignored with --unlit")
setup_render(scene, args)
bpy.ops.render.render(write_still=True)