JiantaoLin
commited on
Commit
·
1e5535f
1
Parent(s):
30d56f8
new
Browse files- shader.py +76 -0
- video_render.py +107 -94
shader.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from pytorch3d.renderer.mesh.shader import ShaderBase
|
| 3 |
+
from pytorch3d.renderer import (
|
| 4 |
+
SoftPhongShader,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
class MultiOutputShader(ShaderBase):
|
| 8 |
+
def __init__(self, device, cameras, lights, materials, ccm_scale=1.0, choices=None):
|
| 9 |
+
super().__init__()
|
| 10 |
+
self.device = device
|
| 11 |
+
self.cameras = cameras
|
| 12 |
+
self.lights = lights
|
| 13 |
+
self.materials = materials
|
| 14 |
+
self.ccm_scale = ccm_scale
|
| 15 |
+
|
| 16 |
+
if choices is None:
|
| 17 |
+
self.choices = ["rgb", "mask", "depth", "normal", "albedo", "ccm"]
|
| 18 |
+
else:
|
| 19 |
+
self.choices = choices
|
| 20 |
+
|
| 21 |
+
self.phong_shader = SoftPhongShader(
|
| 22 |
+
device=self.device,
|
| 23 |
+
cameras=self.cameras,
|
| 24 |
+
lights=self.lights,
|
| 25 |
+
materials=self.materials
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def forward(self, fragments, meshes, **kwargs):
|
| 29 |
+
batch_size, H, W, _ = fragments.zbuf.shape
|
| 30 |
+
output = {}
|
| 31 |
+
|
| 32 |
+
if "rgb" in self.choices:
|
| 33 |
+
rgb_images = self.phong_shader(fragments, meshes, **kwargs)
|
| 34 |
+
rgb = rgb_images[..., :3]
|
| 35 |
+
output["rgb"] = rgb
|
| 36 |
+
|
| 37 |
+
if "mask" in self.choices:
|
| 38 |
+
alpha = rgb_images[..., 3:4]
|
| 39 |
+
mask = (alpha > 0).float()
|
| 40 |
+
output["mask"] = mask
|
| 41 |
+
|
| 42 |
+
if "albedo" in self.choices:
|
| 43 |
+
albedo = meshes.sample_textures(fragments)
|
| 44 |
+
output["albedo"] = albedo[..., 0, :]
|
| 45 |
+
|
| 46 |
+
if "depth" in self.choices:
|
| 47 |
+
depth = fragments.zbuf
|
| 48 |
+
output["depth"] = depth
|
| 49 |
+
|
| 50 |
+
if "normal" in self.choices:
|
| 51 |
+
pix_to_face = fragments.pix_to_face[..., 0]
|
| 52 |
+
bary_coords = fragments.bary_coords[..., 0, :]
|
| 53 |
+
valid_mask = pix_to_face >= 0
|
| 54 |
+
face_indices = pix_to_face[valid_mask]
|
| 55 |
+
faces_packed = meshes.faces_packed()
|
| 56 |
+
normals_packed = meshes.verts_normals_packed()
|
| 57 |
+
face_vertex_normals = normals_packed[faces_packed[face_indices]]
|
| 58 |
+
bary = bary_coords.view(-1, 3)[valid_mask.view(-1)]
|
| 59 |
+
interpolated_normals = (
|
| 60 |
+
bary[..., 0:1] * face_vertex_normals[:, 0, :] +
|
| 61 |
+
bary[..., 1:2] * face_vertex_normals[:, 1, :] +
|
| 62 |
+
bary[..., 2:3] * face_vertex_normals[:, 2, :]
|
| 63 |
+
)
|
| 64 |
+
interpolated_normals = interpolated_normals / interpolated_normals.norm(dim=-1, keepdim=True)
|
| 65 |
+
normal = torch.zeros(batch_size, H, W, 3, device=self.device)
|
| 66 |
+
normal[valid_mask] = interpolated_normals
|
| 67 |
+
output["normal"] = normal
|
| 68 |
+
|
| 69 |
+
if "ccm" in self.choices:
|
| 70 |
+
face_vertices = meshes.verts_packed()[meshes.faces_packed()]
|
| 71 |
+
faces_at_pixels = face_vertices[fragments.pix_to_face]
|
| 72 |
+
ccm = torch.sum(fragments.bary_coords.unsqueeze(-1) * faces_at_pixels, dim=-2)
|
| 73 |
+
ccm = (ccm[..., 0, :] * self.ccm_scale + 1) / 2
|
| 74 |
+
output["ccm"] = ccm
|
| 75 |
+
|
| 76 |
+
return output
|
video_render.py
CHANGED
|
@@ -1,28 +1,28 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
import numpy as np
|
| 4 |
import imageio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import trimesh
|
| 6 |
-
import pyrender
|
| 7 |
from tqdm import tqdm
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
Render a rotating 3D model (OBJ file) to a video with RGB and normal map side-by-side.
|
| 14 |
-
|
| 15 |
-
Args:
|
| 16 |
-
input_obj_path (str): Path to the input OBJ file.
|
| 17 |
-
output_video_path (str): Path to save the output video.
|
| 18 |
-
fps (int): Frames per second for the video.
|
| 19 |
-
frame_count (int): Number of frames in the video.
|
| 20 |
-
resolution (tuple): Resolution of the rendered video (width, height).
|
| 21 |
-
|
| 22 |
-
Returns:
|
| 23 |
-
str: Path to the output video.
|
| 24 |
-
"""
|
| 25 |
-
# 检查输入文件是否存在
|
| 26 |
if not os.path.exists(input_obj_path):
|
| 27 |
raise FileNotFoundError(f"Input OBJ file not found: {input_obj_path}")
|
| 28 |
|
|
@@ -39,85 +39,98 @@ def render_video_from_obj(input_obj_path, output_video_path, fps=15, frame_count
|
|
| 39 |
if not hasattr(mesh_data, 'vertex_normals') or mesh_data.vertex_normals is None:
|
| 40 |
mesh_data.compute_vertex_normals()
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# 设置摄像机参数
|
| 48 |
-
camera = pyrender.PerspectiveCamera(yfov=np.deg2rad(30), znear=0.0001, zfar=100000.0)
|
| 49 |
-
camera_pose = np.eye(4)
|
| 50 |
-
camera_pose[2, 3] = 4.0 # 距离模型 20 个单位
|
| 51 |
-
render_scene.add(camera, pose=camera_pose)
|
| 52 |
-
|
| 53 |
-
# 添加全局环境光
|
| 54 |
-
ambient_light = np.array([1.0, 1.0, 1.0]) * 2.0
|
| 55 |
-
render_scene.ambient_light = ambient_light
|
| 56 |
|
| 57 |
-
#
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
#
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
)
|
| 68 |
|
| 69 |
-
#
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
normal_scene.ambient_light = ambient_light
|
| 75 |
-
|
| 76 |
-
# 初始化渲染器
|
| 77 |
-
r = pyrender.OffscreenRenderer(*resolution)
|
| 78 |
-
|
| 79 |
-
# 创建视频写入器
|
| 80 |
-
writer = imageio.get_writer(output_video_path, fps=fps)
|
| 81 |
-
|
| 82 |
# 渲染每一帧
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
[-math.sin(angle), 0, math.cos(angle), 0],
|
| 91 |
-
[0, 0, 0, 1]
|
| 92 |
-
])
|
| 93 |
-
|
| 94 |
-
# 更新模型的姿态
|
| 95 |
-
render_scene.set_pose(mesh_node, rotation_matrix)
|
| 96 |
-
|
| 97 |
-
# 渲染 RGB 图像
|
| 98 |
-
color, _ = r.render(render_scene)
|
| 99 |
-
|
| 100 |
-
# 更新法线场景的姿态
|
| 101 |
-
normal_scene.set_pose(normal_mesh_node, rotation_matrix)
|
| 102 |
-
|
| 103 |
-
# 渲染法线图像
|
| 104 |
-
normal, _ = r.render(normal_scene, flags=pyrender.RenderFlags.FLAT)
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
if __name__ == '__main__':
|
| 120 |
# 示例调用
|
| 121 |
-
input_obj_path = "
|
| 122 |
output_video_path = "output.mp4"
|
| 123 |
render_video_from_obj(input_obj_path, output_video_path)
|
|
|
|
| 1 |
+
import pytorch3d
|
| 2 |
+
import torch
|
|
|
|
| 3 |
import imageio
|
| 4 |
+
import numpy as np
|
| 5 |
+
import os
|
| 6 |
+
from pytorch3d.io import load_objs_as_meshes
|
| 7 |
+
from pytorch3d.renderer import (
|
| 8 |
+
AmbientLights,
|
| 9 |
+
PerspectiveCameras,
|
| 10 |
+
RasterizationSettings,
|
| 11 |
+
look_at_view_transform,
|
| 12 |
+
TexturesVertex,
|
| 13 |
+
MeshRenderer,
|
| 14 |
+
Materials,
|
| 15 |
+
MeshRasterizer,
|
| 16 |
+
SoftPhongShader,
|
| 17 |
+
PointLights
|
| 18 |
+
)
|
| 19 |
import trimesh
|
|
|
|
| 20 |
from tqdm import tqdm
|
| 21 |
+
from pytorch3d.transforms import RotateAxisAngle
|
| 22 |
+
|
| 23 |
+
from shader import MultiOutputShader
|
| 24 |
+
|
| 25 |
+
def render_video_from_obj(obj_path, output_video_path, num_frames=60, image_size=512, fps=30, device="cuda"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
if not os.path.exists(input_obj_path):
|
| 27 |
raise FileNotFoundError(f"Input OBJ file not found: {input_obj_path}")
|
| 28 |
|
|
|
|
| 39 |
if not hasattr(mesh_data, 'vertex_normals') or mesh_data.vertex_normals is None:
|
| 40 |
mesh_data.compute_vertex_normals()
|
| 41 |
|
| 42 |
+
# 获取顶点坐标、法线和面
|
| 43 |
+
vertices = torch.tensor(mesh_data.vertices, dtype=torch.float32, device=device)
|
| 44 |
+
faces = torch.tensor(mesh_data.faces, dtype=torch.int64, device=device)
|
| 45 |
+
vertex_normals = torch.tensor(mesh_data.vertex_normals, dtype=torch.float32)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
# 获取顶点颜色
|
| 48 |
+
if mesh_data.visual.vertex_colors is None:
|
| 49 |
+
# 如果没有顶点颜色,可以给定一个默认值(例如,白色)
|
| 50 |
+
vertex_colors = torch.ones_like(vertices)[None]
|
| 51 |
+
else:
|
| 52 |
+
vertex_colors = torch.tensor(mesh_data.visual.vertex_colors[:, :3], dtype=torch.float32)[None]
|
| 53 |
+
# 创建纹理并分配顶点颜色
|
| 54 |
+
textures = TexturesVertex(verts_features=vertex_colors)
|
| 55 |
+
textures.to(device)
|
| 56 |
+
# 创建Mesh对象
|
| 57 |
+
mesh = pytorch3d.structures.Meshes(verts=[vertices], faces=[faces], textures=textures)
|
| 58 |
+
|
| 59 |
+
# 设置渲染器
|
| 60 |
+
lights = AmbientLights(ambient_color=((3.0,)*3,), device=device)
|
| 61 |
+
# lights = PointLights(device=device, location=[[0.0, 0.0, 3.0]], ambient_color=[[0.5, 0.5, 0.5]], diffuse_color=[[1.0, 1.0, 1.0]])
|
| 62 |
+
raster_settings = RasterizationSettings(
|
| 63 |
+
image_size=image_size, # 渲染图像的尺寸
|
| 64 |
+
blur_radius=0.0, # 默认无模糊
|
| 65 |
+
faces_per_pixel=1, # 每像素渲染一个面
|
| 66 |
+
# background_color=(1.0, 1.0, 1.0)
|
| 67 |
)
|
| 68 |
|
| 69 |
+
# 设置旋转和渲染参数
|
| 70 |
+
frames = []
|
| 71 |
+
camera_distance = 6.5
|
| 72 |
+
elevs = 0.0
|
| 73 |
+
center = (0.0, 0.0, 0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# 渲染每一帧
|
| 75 |
+
materials = Materials(
|
| 76 |
+
device=device,
|
| 77 |
+
diffuse_color=((0.0, 0.0, 0.0),),
|
| 78 |
+
ambient_color=((1.0, 1.0, 1.0),),
|
| 79 |
+
specular_color=((0.0, 0.0, 0.0),),
|
| 80 |
+
shininess=0.0,
|
| 81 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
rasterizer = MeshRasterizer(raster_settings=raster_settings)
|
| 84 |
+
for i in tqdm(range(num_frames)):
|
| 85 |
+
azims = 360.0 * i / num_frames
|
| 86 |
+
R, T = look_at_view_transform(
|
| 87 |
+
dist=camera_distance,
|
| 88 |
+
elev=elevs,
|
| 89 |
+
azim=azims,
|
| 90 |
+
at=(center,),
|
| 91 |
+
degrees=True
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# 手动设置相机的旋转矩阵
|
| 96 |
+
cameras = PerspectiveCameras(device=device, R=R, T=T, focal_length=5.0)
|
| 97 |
+
cameras.znear = 0.0001
|
| 98 |
+
cameras.zfar = 10000000.0
|
| 99 |
+
shader=MultiOutputShader(
|
| 100 |
+
device=device,
|
| 101 |
+
cameras=cameras,
|
| 102 |
+
lights=lights,
|
| 103 |
+
materials=materials,
|
| 104 |
+
choices=["rgb", "mask", "normal"]
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
renderer = MeshRenderer(rasterizer=rasterizer, shader=shader)
|
| 108 |
+
# 渲染RGB图像和Normal图像
|
| 109 |
+
render_result = renderer(mesh, cameras=cameras)
|
| 110 |
+
rgb_image = render_result["rgb"] * render_result["mask"] + (1 - render_result["mask"]) * torch.ones_like(render_result["rgb"]) * 255.0
|
| 111 |
+
normal_map = render_result["normal"]
|
| 112 |
+
|
| 113 |
+
# 提取RGB和Normal map
|
| 114 |
+
rgb = rgb_image[0, ..., :3].cpu().numpy() # RGB图像
|
| 115 |
+
normal_map = torch.nn.functional.normalize(normal_map, dim=-1) # Normal map
|
| 116 |
+
normal_map = (normal_map + 1) / 2
|
| 117 |
+
normal_map = normal_map * render_result["mask"] + (1 - render_result["mask"]) * torch.ones_like(render_result["normal"])
|
| 118 |
+
normal = normal_map[0, ..., :3].cpu().numpy() # Normal map
|
| 119 |
+
rgb = np.clip(rgb, 0, 255).astype(np.uint8)
|
| 120 |
+
normal = np.clip(normal*255, 0, 255).astype(np.uint8)
|
| 121 |
+
# 将RGB和Normal map合并为一张图,左边RGB,右边Normal map
|
| 122 |
+
combined_image = np.concatenate((rgb, normal), axis=1)
|
| 123 |
+
|
| 124 |
+
# 将合并后的图像加入到帧列表
|
| 125 |
+
frames.append(combined_image)
|
| 126 |
+
|
| 127 |
+
# 使用imageio保存视频
|
| 128 |
+
imageio.mimsave(output_video_path, frames, fps=fps)
|
| 129 |
+
|
| 130 |
+
print(f"Video saved to {output_video_path}")
|
| 131 |
|
| 132 |
if __name__ == '__main__':
|
| 133 |
# 示例调用
|
| 134 |
+
input_obj_path = "/hpc2hdd/home/jlin695/code/github/Kiss3DGen/outputs/a_owl_wearing_a_hat/ISOMER/rgb_projected.obj"
|
| 135 |
output_video_path = "output.mp4"
|
| 136 |
render_video_from_obj(input_obj_path, output_video_path)
|