📹 CameraViewport#

概述#

CameraViewport 允许您在渲染窗口中创建多个相机视口组件,用于同时显示不同相机的实时渲染画面。这对于多角度观察、传感器数据可视化等场景非常有用。

右上角展示了使用 Camera Viewport Widget 组件显示的 RGBD 传感器画面

主要特性#

  • 多视口支持: 在同一窗口中创建多个独立的相机视口

  • 灵活布局: 支持像素、百分比和自动三种布局格式

  • 动态更新: 实时更新视口的相机、布局和属性

基础创建方法#

使用 render.widgets.create_camera_viewport() 方法创建相机视口 widget:

viewport = render.widgets.create_camera_viewport(
    camera=cameras[0],
    layout=Layout(left=10, top=10, width=240, height=180),
    sim_world_index=0
)

参数说明

  • camera: 要显示的相机对象(必需)

  • layout: 布局配置(可选,默认为 left=50, top=50, width=200, height=200)

  • sim_world_index: 模拟世界索引(可选,默认为 0)

返回值:返回一个 CameraViewport 对象。

创建多个 Viewport#

您可以在同一窗口中创建多个 viewport,每个显示不同相机的画面:

# Demonstrates fixed pixel positioning and sizing
vp1 = render.widgets.create_camera_viewport(
    cameras[0], layout=Layout(left=10, top=10, width=240, height=180), sim_world_index=0
)

# Viewport 2: Top-right with percentage-based layout
# Demonstrates responsive positioning using percentages
vp2 = render.widgets.create_camera_viewport(
    cameras[1], layout=Layout(left="60%", top=10, width=240, height=180), sim_world_index=0
)

# Viewport 3: Bottom-left with mixed layout (pixels + percentage)
# Demonstrates mixed layout values
vp3 = render.widgets.create_camera_viewport(
    cameras[0], layout=Layout(left=10, top="60%", width=320, height=240), sim_world_index=0
)

这段代码创建了三个 viewport:

  • vp1: 左上角,使用像素布局,显示 cameras[0]

  • vp2: 右上角,使用百分比布局,显示 cameras[1]

  • vp3: 左下角,使用混合布局,显示 cameras[0]

camera viewport

Widget 动态更新#

创建 widget 后,可以通过 update() 方法动态更新其属性。

更新 Viewport 属性#

CameraViewport.update() 方法支持以下参数:

  • camera: 新的相机对象

  • layout: 新的布局配置

  • sim_world_index: 新的模拟世界索引

所有参数都是可选的,只提供需要更新的参数即可。

更新相机#

切换 viewport 显示的相机:

viewport.update(camera=cameras[1])

更新布局#

修改 viewport 的位置和大小:

viewport.update(layout=Layout(left=100, top=100, width=300, height=200))

组合更新#

可以同时更新多个属性:

viewport.update(
    camera=cameras[2],
    layout=Layout(left=200, top=200),
    sim_world_index=0
)

交互式控制示例#

以下示例展示了如何通过按键交互来更新 widget:

切换相机#

if render.input.is_key_just_pressed("1"):
    vp1.update(camera=cameras[0])
    print("vp1: switched to camera 0")

if render.input.is_key_just_pressed("2"):
    vp1.update(camera=cameras[1])
    print("vp1: switched to camera 1")

if render.input.is_key_just_pressed("3"):
    if len(cameras) >= 3:
        vp1.update(camera=cameras[2])
        print("vp1: switched to camera 2")
    else:
        print("vp1: camera 2 not available")

按 1/2/3 键可以切换 vp1 显示的相机。

移动 Viewport 位置#

move_step = 10
if render.input.is_key_just_pressed("w"):
    vp1_top = max(0, vp1_top - move_step)
    if vp1_visible:
        vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
    print(f"vp1: moved to ({vp1_left}, {vp1_top})")

if render.input.is_key_just_pressed("s"):
    vp1_top += move_step
    if vp1_visible:
        vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
    print(f"vp1: moved to ({vp1_left}, {vp1_top})")

if render.input.is_key_just_pressed("a"):
    vp1_left = max(0, vp1_left - move_step)
    if vp1_visible:
        vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
    print(f"vp1: moved to ({vp1_left}, {vp1_top})")

if render.input.is_key_just_pressed("d"):
    vp1_left += move_step
    if vp1_visible:
        vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
    print(f"vp1: moved to ({vp1_left}, {vp1_top})")

按 w/a/s/d 键可以移动 vp1 的位置。

调整 Viewport 大小#

resize_step = 20
if render.input.is_key_just_pressed("=") or render.input.is_key_just_pressed("+"):
    vp1_width += resize_step
    vp1_height += resize_step * 3 // 4
    if vp1_visible:
        vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
    print(f"vp1: resized to {vp1_width}x{vp1_height}")

if render.input.is_key_just_pressed("-") or render.input.is_key_just_pressed("_"):
    vp1_width = max(100, vp1_width - resize_step)
    vp1_height = max(75, vp1_height - resize_step * 3 // 4)
    if vp1_visible:
        vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
    print(f"vp1: resized to {vp1_width}x{vp1_height}")

按 +/- 键可以调整 vp1 的大小。

移除 Viewport#

如果不再需要某个 viewport,可以使用 remove() 方法将其完全从渲染窗口中移除:

viewport.remove()

警告

调用 remove() 方法后,该 viewport 将被永久移除。后续对该 viewport 对象调用 update() 方法将导致错误。如果需要重新显示,必须重新创建 viewport。

以下示例展示了如何通过按键交互来移除 viewport:

if render.input.is_key_just_pressed("k"):
    vp3.remove()
    print("vp3: removed from screen")

按 k 键可以移除 vp3。移除后,vp3 将从屏幕上完全消失,且无法通过 update 方法恢复。

完整示例#

以下是一个完整的交互式 CameraViewport widget 系统,包含创建、更新、交互控制等功能:

#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""
Widgets Demo - Camera Viewport Example

This example demonstrates the widgets system with camera viewport widgets.

Features demonstrated:
- Creating multiple camera viewports with different layouts
- Pixel-based, percentage-based, and auto layouts
- Interactive viewport manipulation via keyboard controls
- Real-time widget updates

Controls:
- 1/2/3: Switch vp1 camera (sensor_camera_index)
- w/a/s/d: Move vp1 viewport (pixel-based movement)
- +/-: Resize vp1 viewport
- r: Reset all layouts
- h: Toggle vp1 visibility (move off/on screen)
- Delete: Remove vp3 from screen (requires restart to restore)
"""

from motrixsim import SceneData, load_model, run, step
from motrixsim.render import Layout, RenderApp


def main():
    # Print instructions
    print("=" * 60)
    print("Widgets Demo - Camera Viewport Example")
    print("=" * 60)
    print("\nControls:")
    print("  1/2/3   - Switch vp1 to camera 0/1/2")
    print("  w/a/s/d - Move vp1 viewport (up/left/down/right)")
    print("  +/-     - Resize vp1 viewport (larger/smaller)")
    print("  r       - Reset all layouts to default")
    print("  h       - Toggle vp1 visibility")
    print("  k  - Remove vp3 from screen (requires restart to restore)")
    print("\nLayout Formats:")
    print("  Pixels:     Layout(left=50, top=50, width=200, height=150)")
    print("  Percentage: Layout(left='10%', top='10%', width='30%', height='30%')")
    print("  Auto:       Layout(width='auto', height='auto')")
    print("=" * 60)
    print()

    # Create render app for visualization
    with RenderApp() as render:
        # Load the scene model
        path = "examples/assets/go1/scene.xml"
        model = load_model(path)

        # Configure cameras for rendering
        # The go1 scene has multiple cameras that can be used as sensor cameras
        cameras = model.cameras
        if len(cameras) >= 2:
            # Camera 0: RGB camera
            cameras[0].set_render_target("image", 320, 240)

            # Camera 1: Depth camera
            cameras[1].set_render_target("image", 320, 240)
            cameras[1].depth_only = True
            cameras[1].set_near_far(0.1, 1.0)

        # Launch the renderer
        render.launch(model)

        # Create simulation data
        data = SceneData(model)

        # ====================================================================
        # Create Camera Viewport Widgets
        # ====================================================================

        # Viewport 1: Top-left with pixel-based layout
        # Demonstrates fixed pixel positioning and sizing
        vp1 = render.widgets.create_camera_viewport(
            cameras[0], layout=Layout(left=10, top=10, width=240, height=180), sim_world_index=0
        )

        # Viewport 2: Top-right with percentage-based layout
        # Demonstrates responsive positioning using percentages
        vp2 = render.widgets.create_camera_viewport(
            cameras[1], layout=Layout(left="60%", top=10, width=240, height=180), sim_world_index=0
        )

        # Viewport 3: Bottom-left with mixed layout (pixels + percentage)
        # Demonstrates mixed layout values
        vp3 = render.widgets.create_camera_viewport(
            cameras[0], layout=Layout(left=10, top="60%", width=320, height=240), sim_world_index=0
        )

        # Store initial layouts for reset functionality
        initial_layouts = {
            "vp1": Layout(left=10, top=10, width=240, height=180),
            "vp2": Layout(left="60%", top=10, width=240, height=180),
            "vp3": Layout(left=10, top="60%", width=320, height=240),
        }

        # Track vp1 current position and size for movement controls
        vp1_left, vp1_top = 10, 10
        vp1_width, vp1_height = 240, 180
        vp1_visible = True

        print("Widgets created:")
        print(f"  vp1: camera 0 at ({vp1_left}, {vp1_top}), size {vp1_width}x{vp1_height}")
        print("  vp2: camera 1 at (60%, 10), size 240x180")
        print("  vp3: camera 0 at (10, 60%), size 320x240")
        print()

        # ====================================================================
        # Define simulation and rendering callbacks
        # ====================================================================

        def phys_step():
            """Physics step callback - runs 4 substeps for stability"""
            step(model, data)

        def render_step():
            """Render step callback - handles input and synchronization"""
            nonlocal vp1_left, vp1_top, vp1_width, vp1_height, vp1_visible

            # ====================================================================
            # Interactive Controls
            # ====================================================================

            # Switch vp1 camera
            if render.input.is_key_just_pressed("1"):
                vp1.update(camera=cameras[0])
                print("vp1: switched to camera 0")

            if render.input.is_key_just_pressed("2"):
                vp1.update(camera=cameras[1])
                print("vp1: switched to camera 1")

            if render.input.is_key_just_pressed("3"):
                if len(cameras) >= 3:
                    vp1.update(camera=cameras[2])
                    print("vp1: switched to camera 2")
                else:
                    print("vp1: camera 2 not available")

            # Move vp1 viewport (10 pixels per keypress)
            move_step = 10
            if render.input.is_key_just_pressed("w"):
                vp1_top = max(0, vp1_top - move_step)
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                print(f"vp1: moved to ({vp1_left}, {vp1_top})")

            if render.input.is_key_just_pressed("s"):
                vp1_top += move_step
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                print(f"vp1: moved to ({vp1_left}, {vp1_top})")

            if render.input.is_key_just_pressed("a"):
                vp1_left = max(0, vp1_left - move_step)
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                print(f"vp1: moved to ({vp1_left}, {vp1_top})")

            if render.input.is_key_just_pressed("d"):
                vp1_left += move_step
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                print(f"vp1: moved to ({vp1_left}, {vp1_top})")

            # Resize vp1 viewport (20 pixels per keypress)
            resize_step = 20
            if render.input.is_key_just_pressed("=") or render.input.is_key_just_pressed("+"):
                vp1_width += resize_step
                vp1_height += resize_step * 3 // 4
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                print(f"vp1: resized to {vp1_width}x{vp1_height}")

            if render.input.is_key_just_pressed("-") or render.input.is_key_just_pressed("_"):
                vp1_width = max(100, vp1_width - resize_step)
                vp1_height = max(75, vp1_height - resize_step * 3 // 4)
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                print(f"vp1: resized to {vp1_width}x{vp1_height}")

            # Reset all layouts
            if render.input.is_key_just_pressed("r"):
                vp1.update(layout=initial_layouts["vp1"])
                vp2.update(layout=initial_layouts["vp2"])
                vp3.update(layout=initial_layouts["vp3"])
                vp1_left, vp1_top = 10, 10
                vp1_width, vp1_height = 240, 180
                vp1_visible = True
                print("All layouts reset to default")

            # Toggle vp1 visibility
            if render.input.is_key_just_pressed("h"):
                vp1_visible = not vp1_visible
                if vp1_visible:
                    vp1.update(layout=Layout(left=vp1_left, top=vp1_top, width=vp1_width, height=vp1_height))
                    print(f"vp1: visible at ({vp1_left}, {vp1_top})")
                else:
                    # Move off-screen to hide
                    vp1.update(layout=Layout(left=-500, top=-500, width=vp1_width, height=vp1_height))
                    print("vp1: hidden")

            # Remove vp3 from screen
            if render.input.is_key_just_pressed("k"):
                vp3.remove()
                print("vp3: removed from screen")

            # Sync render with simulation
            render.sync(data)

        # ====================================================================
        # Run the main simulation loop
        # ====================================================================
        run.render_loop(model.options.timestep, 60, phys_step, render_step)


相关 API 链接#