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) |