File size: 4,331 Bytes
02a9751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import os
import math
import numpy as np
import imageio
import trimesh
import pyrender
from tqdm import tqdm
# os.environ["CUDA_VISIBLE_DEVICES"] = "7"
os.environ['PYOPENGL_PLATFORM'] = 'egl'  # 设置渲染环境为 EGL(无头模式)

def render_video_from_obj(input_obj_path, output_video_path, fps=15, frame_count=60, resolution=(512, 512)):
    """
    Render a rotating 3D model (OBJ file) to a video with RGB and normal map side-by-side.
    
    Args:
        input_obj_path (str): Path to the input OBJ file.
        output_video_path (str): Path to save the output video.
        fps (int): Frames per second for the video.
        frame_count (int): Number of frames in the video.
        resolution (tuple): Resolution of the rendered video (width, height).
        
    Returns:
        str: Path to the output video.
    """
    # 检查输入文件是否存在
    if not os.path.exists(input_obj_path):
        raise FileNotFoundError(f"Input OBJ file not found: {input_obj_path}")

    # 加载3D模型
    scene_data = trimesh.load(input_obj_path)

    # 提取或合并网格
    if isinstance(scene_data, trimesh.Scene):
        mesh_data = trimesh.util.concatenate([geom for geom in scene_data.geometry.values()])
    else:
        mesh_data = scene_data

    # 确保顶点法线存在
    if not hasattr(mesh_data, 'vertex_normals') or mesh_data.vertex_normals is None:
        mesh_data.compute_vertex_normals()

    # 创建 Pyrender 场景并设置背景为白色
    render_scene = pyrender.Scene(bg_color=[1.0, 1.0, 1.0])
    mesh = pyrender.Mesh.from_trimesh(mesh_data, smooth=True)
    mesh_node = render_scene.add(mesh)

    # 设置摄像机参数
    camera = pyrender.PerspectiveCamera(yfov=np.deg2rad(30), znear=0.0001, zfar=100000.0)
    camera_pose = np.eye(4)
    camera_pose[2, 3] = 4.0  # 距离模型 20 个单位
    render_scene.add(camera, pose=camera_pose)

    # 添加全局环境光
    ambient_light = np.array([1.0, 1.0, 1.0]) * 2.0
    render_scene.ambient_light = ambient_light

    # 准备法线渲染场景
    normals = mesh_data.vertex_normals.copy()

    # 将法线映射到颜色范围 [0, 255]
    normal_colors = ((normals + 1) / 2 * 255)

    # 创建用于法线渲染的独立网格
    normal_mesh_data = mesh_data.copy()
    normal_mesh_data.visual.vertex_colors = np.hstack(
        [normal_colors, np.full((normals.shape[0], 1), 255, dtype=np.uint8)]  # 添加 Alpha 通道
    )

    # 创建法线渲染场景
    normal_scene = pyrender.Scene(bg_color=[1.0, 1.0, 1.0, 1.0])
    normal_mesh = pyrender.Mesh.from_trimesh(normal_mesh_data, smooth=True)
    normal_mesh_node = normal_scene.add(normal_mesh)
    normal_scene.add(camera, pose=camera_pose)
    normal_scene.ambient_light = ambient_light

    # 初始化渲染器
    r = pyrender.OffscreenRenderer(*resolution)

    # 创建视频写入器
    writer = imageio.get_writer(output_video_path, fps=fps)

    # 渲染每一帧
    try:
        for frame_idx in tqdm(range(frame_count)):
            # 计算旋转角度
            angle = 2 * np.pi * frame_idx / frame_count
            rotation_matrix = np.array([
                [math.cos(angle), 0, math.sin(angle), 0],
                [0, 1, 0, 0],
                [-math.sin(angle), 0, math.cos(angle), 0],
                [0, 0, 0, 1]
            ])

            # 更新模型的姿态
            render_scene.set_pose(mesh_node, rotation_matrix)

            # 渲染 RGB 图像
            color, _ = r.render(render_scene)

            # 更新法线场景的姿态
            normal_scene.set_pose(normal_mesh_node, rotation_matrix)

            # 渲染法线图像
            normal, _ = r.render(normal_scene, flags=pyrender.RenderFlags.FLAT)
        
            # 拼接左右图像
            combined_frame = np.concatenate((color, normal), axis=1)

            # 写入视频帧
            writer.append_data(combined_frame)
    finally:
        # 释放资源
        writer.close()
        r.delete()

    print(f"Rendered video saved to {output_video_path}")
    return output_video_path

if __name__ == '__main__':
    # 示例调用
    input_obj_path = "output/gradio_cache/text_3D/_超级赛亚人_10/rgb_projected.obj"
    output_video_path = "output.mp4"
    render_video_from_obj(input_obj_path, output_video_path)