|
30 | 30 | VERSION = "real-sas-toon-shader-plus-silhouette-ink-v1" |
31 | 31 |
|
32 | 32 |
|
| 33 | +def choose_ortho_scale(base_scale, span_x, span_y, *, target_min=0.58, |
| 34 | + target_max=0.86): |
| 35 | + """Correct source-space framing by actual projected source bounds. |
| 36 | +
|
| 37 | + Object corner bounds can overestimate visible silhouette. Real PNG QA is |
| 38 | + still authoritative for legibility and crop, not these estimates. |
| 39 | + """ |
| 40 | + require(all(math.isfinite(x) and x>0 for x in |
| 41 | + (base_scale,span_x,span_y)),"INVALID_PROJECTED_BOUNDS") |
| 42 | + # Scale is linear inverse of projected occupied fraction. |
| 43 | + # If both target spans cannot fit, favor no cropping over minimum span. |
| 44 | + ratio=max(min(span_x,span_y)/target_min, |
| 45 | + max(span_x,span_y)/target_max) |
| 46 | + require(math.isfinite(ratio) and ratio>0,"INVALID_ORTHO_RATIO") |
| 47 | + return max(0.20, min(10000., base_scale*ratio)) |
| 48 | + |
| 49 | + |
| 50 | +def set_source_space_orbit_frame(bpy, camera, meshes, base_scale): |
| 51 | + from mathutils import Vector |
| 52 | + cam_inv=camera.matrix_world.inverted() |
| 53 | + xyz=[] |
| 54 | + for obj in meshes: |
| 55 | + for co in obj.bound_box: |
| 56 | + xyz.append(cam_inv @ (obj.matrix_world @ Vector(co))) |
| 57 | + require(xyz,"NO_MESH_CAMERA_BOUNDS") |
| 58 | + # At square resolution with an orthographic camera, width and height |
| 59 | + # span fractions are both world-space projected span/ortho_scale. |
| 60 | + raw_span_x=(max(x.x for x in xyz)-min(x.x for x in xyz))/base_scale |
| 61 | + raw_span_y=(max(x.y for x in xyz)-min(x.y for x in xyz))/base_scale |
| 62 | + scale=choose_ortho_scale(base_scale,raw_span_x,raw_span_y) |
| 63 | + camera.data.ortho_scale=scale |
| 64 | + return { |
| 65 | + "projected_source_bbox_fraction_x_before_adjustment":raw_span_x, |
| 66 | + "projected_source_bbox_fraction_y_before_adjustment":raw_span_y, |
| 67 | + "source_bbox_correction_based_on_not_human_court":True, |
| 68 | + "camera_ortho_scale":scale, |
| 69 | + } |
| 70 | + |
| 71 | + |
33 | 72 | def image_ink(source: Path, output: Path): |
34 | 73 | from PIL import Image, ImageChops, ImageEnhance, ImageFilter, ImageOps |
35 | 74 | im = Image.open(source).convert("RGBA") |
@@ -93,6 +132,8 @@ def render_orbit(bpy, *, source: Path, expected_sha256: str, |
93 | 132 | cam.name = "SOURCE_SPACE_ORBIT__ANATOMICAL_FRONT_UNKNOWN" |
94 | 133 | cam.data.type = "ORTHO" |
95 | 134 | cam.data.ortho_scale = max(2.15 * radius, 0.5) |
| 135 | + base_ortho_scale = float(cam.data.ortho_scale) |
| 136 | + scene_meshes = [ob for ob in bpy.data.objects if ob.type == "MESH"] |
96 | 137 | bpy.context.scene.camera = cam |
97 | 138 | scene = configure_scene(bpy, resolution) |
98 | 139 | scene.render.engine = "CYCLES" |
@@ -126,6 +167,9 @@ def render_orbit(bpy, *, source: Path, expected_sha256: str, |
126 | 167 | cam.location = center + direction.normalized()*max(radius*3.7, 1.0) |
127 | 168 | cam.rotation_euler = (center-cam.location).to_track_quat("-Z", "Y").to_euler() |
128 | 169 | bpy.context.view_layer.update() |
| 170 | + framing = set_source_space_orbit_frame( |
| 171 | + bpy, cam, scene_meshes, base_ortho_scale) |
| 172 | + bpy.context.view_layer.update() |
129 | 173 | matrix = finite_matrix(cam.matrix_world) |
130 | 174 | for ob,idx,mat in original_materials: |
131 | 175 | ob.material_slots[idx].material = mat |
@@ -158,6 +202,7 @@ def render_orbit(bpy, *, source: Path, expected_sha256: str, |
158 | 202 | "view_id": "SOURCE_ORBIT_%02d" % i, |
159 | 203 | "source_space_orbit_degrees": 45*i, |
160 | 204 | "camera_source_space_matrix": matrix, |
| 205 | + "camera_projection_details": framing, |
161 | 206 | "camera_axis_authored_front_qualified": False, |
162 | 207 | "camera_source_up_qualified": False, |
163 | 208 | "source_reference_png": source_path.name, |
|
0 commit comments