-
Notifications
You must be signed in to change notification settings - Fork 304
/
Copy pathdistort_mesh.py
264 lines (205 loc) · 8.67 KB
/
distort_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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env python3
# In Blender, perform mesh modifications.
#
# Usage:
# This script is to be run from within the Blender GUI. Tested in Blender
# 2.92.
# distort_mesh.py <mesh_path> <object_prefix> [distort_extent] [method]
# where
# mesh_path: Absolute path to mesh file
# object_prefix: Prefix of object name in Scene Collection in Blender. The
# prefix instead of the name is required, because if the script is
# repeatedly run, Blender will append numerical suffixes .001, .002, etc.
# to the object name.
# distort_extent: A floating point number in the range of [0, 1]
# method: A list of strings. Distortion operations, in order of desired
# execution.
#
# Example from command line:
# $ blender -b -P distort_mesh.py -- /path/to/mesh.obj object_prefix 0.005 "['subdiv_mod', 'vert_rand', 'edge_subdiv']"
#
# Example from Blender Scripting GUI Python command line:
# >>> file_path = '/path/to/mesh.dae'
# >>> object_prefix = 'Cube'
# >>> distort_extent = 0.005 # float in range [0, 1]
# >>> method = ['subdiv_mod', 'vert_rand', 'edge_subdiv']
# >>> sys.argv = [file_path, object_prefix, distort_extent, method]
# >>> exec(open('/path/to/distort_mesh.py').read());
#
import bpy
import os
import sys
import numpy as np
def find_target_object(object_prefix):
target_obj = None
object_name = None
# Find the object that matches the name prefix. Assume the first one.
for obj in bpy.data.objects:
if obj.name.startswith(object_prefix):
target_obj = bpy.data.objects[obj.name]
object_name = obj.name
break
if target_obj == None:
print('ERROR: Object with prefix [{}] not found'.format(object_prefix))
return None
return target_obj, object_name
# shading: 'WIREFRAME', 'SOLID', 'MATERIAL', or 'RENDERED'
def viewport_shading(shading):
print('Changing viewport shading to {}...'.format(shading))
areas = bpy.context.workspace.screens[0].areas
for ar in areas:
for spc in ar.spaces:
if spc.type == 'VIEW_3D':
spc.shading.type = shading
def subdivision_modifier(obj, levels=2):
if levels == 0:
print('Subdivision modifier level is 0, skipping')
return
print('Applying subdivision modifier with level {}...'.format(levels))
viewport_shading('WIREFRAME')
# Select the object
obj.select_set(True)
# Set active object, otherwise incorrect context
bpy.context.view_layer.objects.active = obj
# subdivision modifier
mod = obj.modifiers.new(name='Subdivision', type='SUBSURF')
mod.subdivision_type = 'SIMPLE'
mod.show_only_control_edges = False
mod.levels = levels
print(bpy.ops.object.modifier_apply(modifier='Subdivision'))
# Randomize mesh vertices
# offset (float in [-inf, inf], (optional)): Amount, Distance to offset. Meters
# uniform (float in [0, 1], (optional)): Uniform, Increase for uniform offset
# distance
# normal (float in [0, 1], (optional)): Normal, Align offset direction to
# normals
# seed (int in [0, 10000], (optional)): Random Seed
def mesh_vert_randomize(obj, offset=0.0, uniform=0.0, normal=1.0, seed=0):
print('Applying mesh vertex randomization with offset {}...'.format(offset))
# Set active object, otherwise incorrect context
bpy.context.view_layer.objects.active = obj
# Go into Edit mode
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Offset is in meters
print(bpy.ops.transform.vertex_random(offset=offset, uniform=uniform,
normal=normal, seed=seed))
# Go back to object mode
bpy.ops.object.mode_set(mode='OBJECT')
def edge_subdivide(obj, ncuts=1, smooth=1.0):
print('Applying edge subdivide with number of cuts {}...'.format(ncuts))
# Set active object, otherwise incorrect context
bpy.context.view_layer.objects.active = obj
# Go into Edit mode
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
print(bpy.ops.mesh.subdivide(number_cuts=ncuts, smoothness=smooth,
ngon=True, quadcorner='STRAIGHT_CUT', fractal=0.0,
fractal_along_normal=0.0, seed=0))
# Go back to object mode
bpy.ops.object.mode_set(mode='OBJECT')
# file_path: Full path to input file
# object_prefix: Prefix of the mesh name found in the 3D model file
# distort_extent: relative scale, in range [0.0, 1.0]
# method: List of strings, a subset of those defined in METHODS
def distort(file_path, object_prefix, distort_extent, method):
# Sanity checks
if not os.path.exists(file_path):
print('ERROR: File does not exist: [{}]'.format(file_path))
return
if distort_extent < 0.0 or distort_extent > 1.0:
print('ERROR: distort_extent ({}) must be in range [0.0, 1.0]'.format(
distort_extent))
return
if not isinstance(method, list):
print('ERROR: method parameter "%s" must be specified as a list' % method)
return
# Clear scene
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Open file
if file_path.lower().endswith('dae'):
bpy.ops.wm.collada_import(filepath=file_path)
elif file_path.lower().endswith('obj'):
bpy.ops.import_scene.obj(filepath=file_path, axis_forward='X',
axis_up='Z')
else:
print('ERROR: Only COLLADA (.dae) and OBJ formats are supported for importing at the moment.')
return
objs = find_target_object(object_prefix)
if objs is None:
print('Error detected. Aborting')
return
target_obj, object_name = objs
print('Distorting mesh [{}] for relative extent {} out of [0, 1]...'.format(
object_name, distort_extent))
METHODS = ['subdiv_mod', 'vert_rand', 'edge_subdiv']
for step in method:
if not step in METHODS:
print('ERROR: Unrecognized step {}'.format(step))
print('Available steps are:')
for m in METHODS:
print(m)
print('Aborting')
break
if step == 'subdiv_mod':
# Figure out levels magnitude
SUBDIV_LVL_MIN = 0
# This might need tuning
SUBDIV_LVL_MAX = 4
# Must be integer
subdiv_lvl = round(SUBDIV_LVL_MIN + (
(SUBDIV_LVL_MAX - SUBDIV_LVL_MIN) * distort_extent))
subdivision_modifier(target_obj, subdiv_lvl)
elif step == 'vert_rand':
# Meters
VERT_RAND_MIN = 0
# Set max in terms of object dimensions. This might need tuning.
VERT_RAND_MAX = 10 * np.max(target_obj.dimensions)
# Figure out offset magnitude
vert_rand_amt = VERT_RAND_MIN + (
(VERT_RAND_MAX - VERT_RAND_MIN) * distort_extent)
mesh_vert_randomize(target_obj, vert_rand_amt, uniform=0.0,
normal=1.0, seed=0)
elif step == 'edge_subdiv':
edge_subdivide(target_obj)
# Export result to file
out_path = os.path.splitext(file_path)[0] + '_distort' + \
os.path.splitext(file_path)[1]
# NOTE: COLLADA is not exporting texture correctly, not sure why.
# NOTE: collada_export() does not expose relative path option.
if out_path.lower().endswith('dae'):
bpy.ops.wm.collada_export(filepath=out_path)
elif out_path.lower().endswith('obj'):
bpy.ops.export_scene.obj(filepath=out_path, path_mode='RELATIVE',
axis_forward='X', axis_up='Z')
else:
print('ERROR: Only COLLADA (.dae) and OBJ formats are supported for exporting at the moment.')
return
print('Exported result to [{}]'.format(out_path))
if __name__ == '__main__':
# Default values
distort_extent = 0.1
method = ['subdiv_mod', 'vert_rand', 'edge_subdiv']
# Parse everything after `--`
argStartI = 0
for i in range(len(sys.argv)):
if sys.argv[i] == '--':
argStartI = i + 1
break
# Parse args
if len(sys.argv) - argStartI < 2:
print('ERROR: Mesh name prefix not provided. No object to distort.')
else:
file_path = sys.argv[argStartI]
object_prefix = sys.argv[argStartI + 1]
if len(sys.argv) - argStartI > 1:
distort_extent = float(sys.argv[argStartI + 2])
if len(sys.argv) - argStartI > 2:
# If arg is not already a list (happens when script run from
# bash command line, for example, as opposed to Blender Python
# prompt), convert string literal of a list, to a list.
if type(sys.argv[argStartI + 3]) != list:
import ast
method = ast.literal_eval(sys.argv[argStartI + 3])
distort(file_path, object_prefix, distort_extent, method)