From a7ed71698368a7e6df1a79f124bd7df8b10970ce Mon Sep 17 00:00:00 2001 From: Jude Southworth Date: Tue, 27 Oct 2026 10:09:48 +0000 Subject: [PATCH] Tweaking settings --- generate-mix.sh | 19 ++++++ generate.sh => generate-value.sh | 11 +++- render_model.py | 108 ++++++++++++++++++++++++++++++- renders/dew_test.png | Bin 0 -> 223017 bytes test-2.png | Bin 0 -> 2019046 bytes test.png | Bin 1986530 -> 1956497 bytes 6 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 generate-mix.sh rename generate.sh => generate-value.sh (64%) create mode 100644 renders/dew_test.png create mode 100644 test-2.png diff --git a/generate-mix.sh b/generate-mix.sh new file mode 100644 index 0000000..e179445 --- /dev/null +++ b/generate-mix.sh @@ -0,0 +1,19 @@ +python3 make_label.py assets/food_apple_01/food_apple_01_1k.blend \ + --texture assets/face_crop.png \ + --project \ + --output test-2.png \ + --azimuth 15 \ + --elevation -5 \ + --margin 0.9 \ + --transparent \ + --project-size 1.35 \ + --project-offset-y 0.05 \ + --project-offset-x -0.05 \ + --blend-mode MIX \ + --light-strength 1 \ + --ambient 0.25 \ + --blend-factor 0.75 \ + --dew 45 \ + --dew-size 0.025 \ + --dew-flatness 0.4 \ + --dew-seed 4 diff --git a/generate.sh b/generate-value.sh similarity index 64% rename from generate.sh rename to generate-value.sh index 53a59e6..129d3bc 100644 --- a/generate.sh +++ b/generate-value.sh @@ -1,7 +1,7 @@ python3 make_label.py assets/food_apple_01/food_apple_01_1k.blend \ --texture assets/face_crop.png \ --project \ - --output test.png \ + --output test-2.png \ --azimuth 15 \ --elevation -5 \ --margin 0.9 \ @@ -10,5 +10,10 @@ python3 make_label.py assets/food_apple_01/food_apple_01_1k.blend \ --project-offset-y 0.05 \ --project-offset-x -0.05 \ --blend-mode VALUE \ - --light-strength 1.5 \ - --blend-factor 1.5 + --light-strength 1 \ + --ambient 0.25 \ + --blend-factor 1 \ + --dew 45 \ + --dew-size 0.025 \ + --dew-flatness 0.4 \ + --dew-seed 4 diff --git a/render_model.py b/render_model.py index 4e1c774..b692380 100644 --- a/render_model.py +++ b/render_model.py @@ -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) diff --git a/renders/dew_test.png b/renders/dew_test.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f74a09be6a95ff21671944aa81ccc22e1d5c55 GIT binary patch literal 223017 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