Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions data/tool-guides/lighting-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Recommended sequence:
2. `validate_studio_scene(target_names)`
3. `render_image(output_path)`

`validate_studio_scene` now checks all eight world-space target bounding-box corners in camera space and reports `camera.frame_bounds`. Treat frame fill warnings as real composition problems:
- "too little" means the model is distant or visually weak; rerun `setup_studio_scene` with a lower `distance_multiplier` or longer focal length.
- "too much" or outside-frame warnings mean the model is cropped; rerun with a higher `distance_multiplier`, wider lens, or larger `frame_margin`.
- For product previews, a fill around 0.35-0.75 usually leaves enough professional breathing room.

Use `add_light` and `set_light_properties` for custom scene lighting after the studio preset is in place or when the user asks for a specific lamp/sun/spot setup.

Use `set_world_environment` for world/background changes after the scene rig is in place:
Expand Down
39 changes: 37 additions & 2 deletions desktop/assets/vipermesh-addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -6287,6 +6287,16 @@ def _studio_bounds(self, target_names=None):
"radius": radius,
}, missing

def _studio_box_corners(self, bounds):
min_corner = bounds["min"]
max_corner = bounds["max"]
return [
mathutils.Vector((x, y, z))
for x in (min_corner.x, max_corner.x)
for y in (min_corner.y, max_corner.y)
for z in (min_corner.z, max_corner.z)
]

def _look_at(self, obj, target):
direction = mathutils.Vector(target) - obj.location
if direction.length > 0:
Expand Down Expand Up @@ -6568,6 +6578,9 @@ def validate_studio_scene(
require_camera=True,
require_lights=True,
require_render_settings=True,
min_frame_fill=0.18,
max_frame_fill=0.92,
frame_margin=0.06,
):
"""Validate that the scene has usable target meshes, lighting, camera framing, and render settings."""
try:
Expand Down Expand Up @@ -6601,12 +6614,34 @@ def validate_studio_scene(
from bpy_extras.object_utils import world_to_camera_view
projected = [
world_to_camera_view(scene, camera, corner)
for corner in (bounds["min"], bounds["max"], bounds["center"])
for corner in self._studio_box_corners(bounds)
]
if any(point.z < 0 for point in projected):
errors.append("Target bounds are behind the camera")
if any(point.x < -0.15 or point.x > 1.15 or point.y < -0.15 or point.y > 1.15 for point in projected):
min_x = min(point.x for point in projected)
max_x = max(point.x for point in projected)
min_y = min(point.y for point in projected)
max_y = max(point.y for point in projected)
frame_width = max_x - min_x
frame_height = max_y - min_y
frame_fill = max(frame_width, frame_height)
frame_bounds = {
"min_x": round(min_x, 6),
"max_x": round(max_x, 6),
"min_y": round(min_y, 6),
"max_y": round(max_y, 6),
"width": round(frame_width, 6),
"height": round(frame_height, 6),
"fill": round(frame_fill, 6),
"margin": round(float(frame_margin), 6),
}
camera_report["frame_bounds"] = frame_bounds
if min_x < float(frame_margin) or max_x > 1.0 - float(frame_margin) or min_y < float(frame_margin) or max_y > 1.0 - float(frame_margin):
warnings.append("Target bounds may be partially outside the camera frame")
if frame_fill < float(min_frame_fill):
warnings.append("Target occupies too little of the camera frame")
if frame_fill > float(max_frame_fill):
warnings.append("Target occupies too much of the camera frame")
except Exception as exc:
warnings.append(f"Could not project target into camera view: {str(exc)}")

Expand Down
12 changes: 12 additions & 0 deletions lib/ai/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2487,19 +2487,28 @@ const validateStudioScene = tool(
require_camera,
require_lights,
require_render_settings,
min_frame_fill,
max_frame_fill,
frame_margin,
}: {
target_names?: string[]
camera_name?: string
require_camera?: boolean
require_lights?: boolean
require_render_settings?: boolean
min_frame_fill?: number
max_frame_fill?: number
frame_margin?: number
}) =>
executeMcpCommand("validate_studio_scene", {
target_names,
camera_name,
require_camera,
require_lights,
require_render_settings,
min_frame_fill,
max_frame_fill,
frame_margin,
}),
{
name: "validate_studio_scene",
Expand All @@ -2511,6 +2520,9 @@ const validateStudioScene = tool(
require_camera: z.boolean().optional().describe("Require a camera; defaults true"),
require_lights: z.boolean().optional().describe("Require render-visible positive-energy lights; defaults true"),
require_render_settings: z.boolean().optional().describe("Check render resolution/settings; defaults true"),
min_frame_fill: z.number().min(0).max(1).optional().describe("Warn when projected target bounds fill less than this normalized frame share"),
max_frame_fill: z.number().min(0).max(2).optional().describe("Warn when projected target bounds fill more than this normalized frame share"),
frame_margin: z.number().min(-1).max(0.5).optional().describe("Required normalized frame margin around projected target bounds"),
}),
}
)
Expand Down
2 changes: 1 addition & 1 deletion lib/orchestration/prompts/blender-agent-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ You have access to the following MCP tools. **Use direct tools whenever one matc
### 💡 Lighting Tools
- `setup_studio_scene(target_names?, preset?, camera_name?, frame_camera?, focal_length?, distance_multiplier?, resolution_x?, resolution_y?)`: Create/update a studio/product lighting rig, camera, and render defaults around target meshes.
- `set_world_environment(mode?, color?, strength?, hdri_path?, rotation_z?, sky_type?, sun_elevation?, sun_rotation?)`: Configure world/background environment lighting. Use for solid background, HDRI rotation/strength, procedural sky, ambient/environment changes.
- `validate_studio_scene(target_names?, camera_name?, require_camera?, require_lights?, require_render_settings?)`: Check camera, framing, lights, and render settings before rendering.
- `validate_studio_scene(target_names?, camera_name?, require_camera?, require_lights?, require_render_settings?, min_frame_fill?, max_frame_fill?, frame_margin?)`: Check camera, projected target frame bounds/fill, lights, and render settings before rendering.
- `render_thumbnail_to_path(output_path?, target_names?, preset?, camera_name?, resolution?, samples?, file_format?, frame_camera?, distance_multiplier?, focal_length?)`: Render a lightweight studio thumbnail/preview artifact. Prefer this over `render_image` for validation previews unless the user asked for a final render.
- `add_light(light_type?, name?, location?, energy?, color?)`: Add POINT, SUN, SPOT, or AREA light.
- `set_light_properties(name, energy?, color?, shadow_soft_size?, spot_size?, spot_blend?, size?)`: Modify existing light.
Expand Down
2 changes: 1 addition & 1 deletion lib/orchestration/tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export const TOOL_REGISTRY: ToolMetadata[] = [
"Validate target meshes, active/requested camera, target framing, render-visible lights, and render settings before render_image.",
category: "lighting",
parameters:
"target_names?: string[], camera_name?: string, require_camera?: boolean, require_lights?: boolean, require_render_settings?: boolean",
"target_names?: string[], camera_name?: string, require_camera?: boolean, require_lights?: boolean, require_render_settings?: boolean, min_frame_fill?: number, max_frame_fill?: number, frame_margin?: number",
},
{
name: "render_thumbnail_to_path",
Expand Down
39 changes: 37 additions & 2 deletions public/downloads/vipermesh-addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -6287,6 +6287,16 @@ def _studio_bounds(self, target_names=None):
"radius": radius,
}, missing

def _studio_box_corners(self, bounds):
min_corner = bounds["min"]
max_corner = bounds["max"]
return [
mathutils.Vector((x, y, z))
for x in (min_corner.x, max_corner.x)
for y in (min_corner.y, max_corner.y)
for z in (min_corner.z, max_corner.z)
]

def _look_at(self, obj, target):
direction = mathutils.Vector(target) - obj.location
if direction.length > 0:
Expand Down Expand Up @@ -6568,6 +6578,9 @@ def validate_studio_scene(
require_camera=True,
require_lights=True,
require_render_settings=True,
min_frame_fill=0.18,
max_frame_fill=0.92,
frame_margin=0.06,
):
"""Validate that the scene has usable target meshes, lighting, camera framing, and render settings."""
try:
Expand Down Expand Up @@ -6601,12 +6614,34 @@ def validate_studio_scene(
from bpy_extras.object_utils import world_to_camera_view
projected = [
world_to_camera_view(scene, camera, corner)
for corner in (bounds["min"], bounds["max"], bounds["center"])
for corner in self._studio_box_corners(bounds)
]
if any(point.z < 0 for point in projected):
errors.append("Target bounds are behind the camera")
if any(point.x < -0.15 or point.x > 1.15 or point.y < -0.15 or point.y > 1.15 for point in projected):
min_x = min(point.x for point in projected)
max_x = max(point.x for point in projected)
min_y = min(point.y for point in projected)
max_y = max(point.y for point in projected)
frame_width = max_x - min_x
frame_height = max_y - min_y
frame_fill = max(frame_width, frame_height)
frame_bounds = {
"min_x": round(min_x, 6),
"max_x": round(max_x, 6),
"min_y": round(min_y, 6),
"max_y": round(max_y, 6),
"width": round(frame_width, 6),
"height": round(frame_height, 6),
"fill": round(frame_fill, 6),
"margin": round(float(frame_margin), 6),
}
camera_report["frame_bounds"] = frame_bounds
if min_x < float(frame_margin) or max_x > 1.0 - float(frame_margin) or min_y < float(frame_margin) or max_y > 1.0 - float(frame_margin):
warnings.append("Target bounds may be partially outside the camera frame")
if frame_fill < float(min_frame_fill):
warnings.append("Target occupies too little of the camera frame")
if frame_fill > float(max_frame_fill):
warnings.append("Target occupies too much of the camera frame")
except Exception as exc:
warnings.append(f"Could not project target into camera view: {str(exc)}")

Expand Down
36 changes: 36 additions & 0 deletions scripts/test/test-blender-studio-framing-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from "node:assert/strict"
import { readFileSync } from "node:fs"

function extractPythonMethod(source: string, methodName: string): string {
const startToken = ` def ${methodName}(`
const start = source.indexOf(startToken)
assert.notEqual(start, -1, `Expected ${methodName} definition`)
const next = source.indexOf("\n def ", start + startToken.length)
return source.slice(start, next === -1 ? source.length : next)
}

for (const [label, source] of [
["desktop addon", readFileSync("desktop/assets/vipermesh-addon.py", "utf8")],
["public addon", readFileSync("public/downloads/vipermesh-addon.py", "utf8")],
] as const) {
const validateBody = extractPythonMethod(source, "validate_studio_scene")
assert.match(validateBody, /_studio_box_corners/, `${label} projects all target bounds corners`)
assert.match(validateBody, /frame_bounds/, `${label} reports normalized camera frame bounds`)
assert.match(validateBody, /min_frame_fill/, `${label} warns when target is too small in frame`)
assert.match(validateBody, /max_frame_fill/, `${label} warns when target is too large in frame`)
assert.match(validateBody, /frame_margin/, `${label} supports configurable camera-frame margin`)
}

const agentSource = readFileSync("lib/ai/agents.ts", "utf8")
assert.match(agentSource, /min_frame_fill/)
assert.match(agentSource, /max_frame_fill/)
assert.match(agentSource, /frame_margin/)

const registrySource = readFileSync("lib/orchestration/tool-registry.ts", "utf8")
assert.match(registrySource, /min_frame_fill/)
assert.match(registrySource, /max_frame_fill/)

const lightingGuide = readFileSync("data/tool-guides/lighting-guide.md", "utf8")
assert.match(lightingGuide, /frame fill/i)

console.log("Blender studio framing validation tests passed")
Loading