-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrender-pcd-with-mesh.py
250 lines (228 loc) · 10.6 KB
/
render-pcd-with-mesh.py
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# blender packages
import bpy
from bpy.types import (
Scene, Material, Object
)
from mathutils import Vector, Euler
# third-party packages
import numpy as np
# built-in modules
import sys
from pathlib import Path
from itertools import product
import logging
from typing import List
def init_scene():
"""
Initialize a scene with the basic rendering configurations.
"""
# the bpy.context module is usually read-only, so we access the current scene through bpy.data
scene_name: str = bpy.context.scene.name
scene: Scene = bpy.data.scenes[scene_name]
scene.render.engine = 'BLENDER_EEVEE'
# output image settings
scene.render.image_settings.color_mode = 'RGBA'
scene.render.image_settings.color_depth = '8'
scene.render.image_settings.file_format = 'PNG'
scene.render.resolution_x = 1024
scene.render.resolution_y = 1024
scene.render.film_transparent = True # transparent background
# remove the default cube and lights created by blender
for obj in bpy.data.objects:
if obj.name != 'Camera':
logging.info(f'remove object {obj.name} from the scene')
bpy.data.objects.remove(obj)
def create_materials():
"""
Create materials for rendering the input point cloud / output mesh
"""
params = [
{'name': 'pointcloud', 'color': (0.165, 0.564, 0.921, 1.0), 'transparent': False},
{'name': 'mesh', 'color': (0.794, 0.489, 0.243, 1.0), 'transparent': True}
]
global protected_material_names
protected_material_names = [param['name'] for param in params]
roughness = 0.5
for param in params:
# create a new material and enable nodes
bpy.data.materials.new(name=param['name'])
material: Material = bpy.data.materials[param['name']]
material.use_nodes = True
nodes: bpy_prop_collection = material.node_tree.nodes
links: bpy_prop_collection = material.node_tree.links
# remove the default Principle BSDF node in the material's node tree
for node in nodes:
if node.type != 'OUTPUT_MATERIAL':
nodes.remove(node)
# add a Diffuse BSDF node
BSDF_node = nodes.new('ShaderNodeBsdfDiffuse')
BSDF_node.inputs['Color'].default_value = param['color']
BSDF_node.inputs['Roughness'].default_value = roughness
output_node: ShaderNodeOutputMaterial = nodes['Material Output']
if param['transparent']:
# for a transparent material, create a Mix Shader node and enable color
# blending
transparent_node = nodes.new('ShaderNodeBsdfTransparent')
mix_node = nodes.new('ShaderNodeMixShader')
mix_node.inputs['Fac'].default_value = 0.5
# here we have to use index instead of key to access the 'Shader' input
# of a Mix Shader node, because there are two input slots with the same
# name 'Shader' and we need to use both of them
links.new(BSDF_node.outputs['BSDF'], mix_node.inputs[1])
links.new(transparent_node.outputs['BSDF'], mix_node.inputs[2])
links.new(mix_node.outputs['Shader'], output_node.inputs['Surface'])
material.blend_method = 'BLEND'
material.shadow_method = 'CLIP'
else:
# for a non-transparent material, link the Diffuse BSDF node's output
# with the output node's input
links.new(BSDF_node.outputs['BSDF'], output_node.inputs['Surface'])
logging.info('Diffuse BSDF material {} has been created'.format(param['name']))
def init_camera():
"""
Set the camera's position
"""
camera_obj: Object = bpy.data.objects['Camera']
# the location is obtained through GUI
camera_obj.location = (0.7359, -0.6926, 0.4958)
def init_lights(scale_factor: float=1):
"""
Set lights for rendering.
By default, this function will place
- two sun lights above the object
- one point light below the object
The object is assumed to be normalized, i.e. it can be enclosed by a unit cube
centered at (0, 0, 0).
To render larger objects, pass the `scale_factor` parameter explicitly to scale
the locations of lights.
"""
# all parameters are obtained through blender GUI
# the unit of angle is radians as blender API default setting
params = [
{
'name': 'sun light 1', 'type': 'SUN',
'location': np.array([3.638, 1.674, 4.329]), 'energy': 5.0, 'angle': 0.199
},
{
'name': 'sun light 2', 'type': 'SUN',
'location': np.array([0.449, -3.534, 1.797]), 'energy': 1.83, 'angle': 0.009
},
{
'name': 'point light 1', 'type': 'POINT',
'location': np.array([-2.163, -0.381, -2.685]), 'energy': 500
}
]
for param in params:
light = bpy.data.lights.new(name=param['name'], type=param['type'])
light.energy = param['energy']
if param['type'] == 'SUN':
light.angle = param['angle']
light_obj = bpy.data.objects.new(name=param['name'], object_data=light)
light_obj.location = param['location'] * scale_factor
bpy.context.collection.objects.link(light_obj)
def create_pointcloud_modifier():
"""
Create the geometry nodes as a modifier for point clouds.
This modifier will expand each point to a ico sphere for rendering.
"""
# create a node group and enable it as a geometry modifier
geom_nodes = bpy.data.node_groups.new('pointcloud modifier', 'GeometryNodeTree')
geom_nodes.is_modifier = True
nodes = geom_nodes.nodes
links = geom_nodes.links
interface = geom_nodes.interface
interface.new_socket('Geometry', in_out='INPUT', socket_type='NodeSocketGeometry')
interface.new_socket('Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry')
# create all node with their properties set
input_node = nodes.new('NodeGroupInput')
output_node = nodes.new('NodeGroupOutput')
mesh_to_points_node = nodes.new('GeometryNodeMeshToPoints')
mesh_to_points_node.mode = 'VERTICES'
ico_sphere_node = nodes.new('GeometryNodeMeshIcoSphere')
ico_sphere_node.inputs['Radius'].default_value = 0.005
ico_sphere_node.inputs['Subdivisions'].default_value = 3 # control the smoothness of the ico sphere
instance_node = nodes.new('GeometryNodeInstanceOnPoints')
material_node = nodes.new('GeometryNodeReplaceMaterial')
# only set the New slot of the Replace Material node because we actually
# use it to set the material of output instances (spheres), the Old slot
# is not used.
material_node.inputs['New'].default_value = bpy.data.materials['pointcloud']
# link the nodes
links.new(input_node.outputs['Geometry'], mesh_to_points_node.inputs['Mesh'])
# the PLY file are imported as mesh, so we need to replace each vertex in
# the mesh with a point, then we will have a real point cloud in Blender
links.new(mesh_to_points_node.outputs['Points'], instance_node.inputs['Points'])
# use the pre-defined ico sphere as the template instance. with the
# Instance On Points node we can instantiate an instance at each point in
# the point cloud
links.new(ico_sphere_node.outputs['Mesh'], instance_node.inputs['Instance'])
links.new(instance_node.outputs['Instances'], material_node.inputs['Geometry'])
links.new(material_node.outputs['Geometry'], output_node.inputs['Geometry'])
def track_object(obj: Object):
"""
Let the camera track the specified object's center.
By setting the tracking constraint, we can easily make the camera orient to
the target object we want to render. This is less flexible but easier than
setting the rotation manually.
"""
camera: Object = bpy.data.objects['Camera']
# the Track To constraint can keep the up direction of the camera better
# than the Damp Track constraint, allowing placing the camera in the half-
# space where x < 0
camera.constraints.new('TRACK_TO')
constraint = camera.constraints['Track To']
constraint.target = obj
constraint.track_axis = 'TRACK_NEGATIVE_Z'
def clear_imported_objects():
"""
Remove the imported mesh and point cloud from the current scene, together
with the materials automatically created by Blender when importing a mesh.
"""
for obj in bpy.data.objects:
# after application of geometry nodes, the point cloud data will also
# be mesh
if obj.type == 'MESH':
logging.info(f'remove object {obj.name} from the current scene')
bpy.data.objects.remove(obj)
for material in bpy.data.materials:
if material.name not in protected_material_names:
logging.info(f'remove material {material.name}')
bpy.data.materials.remove(material)
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
init_scene()
create_materials()
init_lights()
create_pointcloud_modifier()
base_path = Path('/home/greyishsong/workspace/osp-output')
mesh_path = base_path / 'obj'
pointcloud_path = Path('/home/greyishsong/workspace/scps-output/ply')
img_path = base_path / 'misleaded'
camera_obj: Object = bpy.data.objects['Camera']
# the location is obtained through GUI
camera_location = np.array([0.7359, -0.6926, 0.4958]) * 1.2
with open(base_path / 'problem-misleaded.txt', 'r') as f:
object_indices = f.readlines()
object_indices: List[str] = [index.strip() for index in object_indices if index.strip() != '']
# for obj_file in data_path.iterdir():
# object_index = obj_file.stem
for object_index in object_indices:
obj_file = mesh_path / f'{object_index}.obj'
ply_file = pointcloud_path / f'{object_index}.ply'
if not obj_file.exists():
continue
logging.info(f'start rendering object {object_index}')
bpy.ops.wm.obj_import(filepath=obj_file.as_posix())
mesh = bpy.data.objects[object_index]
mesh.active_material = bpy.data.materials['mesh']
bpy.ops.wm.ply_import(filepath=ply_file.as_posix(), forward_axis='NEGATIVE_Z', up_axis='Y')
pointcloud = bpy.data.objects[f'{object_index}.001']
modifier = pointcloud.modifiers.new('modifier', 'NODES')
modifier.node_group = bpy.data.node_groups['pointcloud modifier']
for view_index, sign in enumerate(product(np.array([1, -1]), repeat=3)):
camera_obj.location = camera_location * sign
track_object(mesh)
bpy.context.scene.render.filepath = (img_path / f'{object_index}-{view_index}.png').as_posix()
bpy.ops.render.render(write_still=True)
clear_imported_objects()
logging.info('done')