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
|
# This file is a part of the HiRISE DTM Importer for Blender
#
# Copyright (C) 2017 Arizona Board of Regents on behalf of the Planetary Image
# Research Laboratory, Lunar and Planetary Laboratory at the University of
# Arizona.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Objects for creating 3D models in Blender"""
import bpy
import bmesh
import numpy as np
from .triangulate import Triangulate
class BTerrain:
"""
Functions for creating Blender meshes from DTM objects
This class contains functions that convert DTM objects to Blender meshes.
Its main responsiblity is to triangulate a mesh from the elevation data in
the DTM. Additionally, it attaches some metadata to the object and creates
a UV map for it so that companion ortho-images drape properly.
This class provides two public methods: `new()` and `reload()`.
`new()` creates a new object[1] and attaches a new mesh to it.
`reload()` replaces the mesh that is attached to an already existing
object. This allows us to retain the location and orientation of the parent
object's coordinate system but to reload the terrain at a different
resolution.
Notes
----------
[1] If you're unfamiliar with Blender, one thing that will help you in
reading this code is knowing the difference between 'meshes' and
'objects'. A mesh is just a collection of vertices, edges and
faces. An object may have a mesh as a child data object and
contains additional information, e.g. the location and orientation
of the coordinate system its child-meshes are reckoned in terms of.
"""
@staticmethod
def new(dtm, name='Terrain'):
"""
Loads a new terrain
Parameters
----------
dtm : DTM
name : str, optional
The name that will be assigned to the new object, defaults
to 'Terrain' (and, if an object named 'Terrain' already
exists, Blender will automatically extend the name of the
new object to something like 'Terrain.001')
Returns
----------
obj : bpy_types.Object
"""
bpy.ops.object.add(type="MESH")
obj = bpy.context.object
obj.name = name
# Fill the object data with a Terrain mesh
obj.data = BTerrain._mesh_from_dtm(dtm)
# Add some meta-information to the object
metadata = BTerrain._create_metadata(dtm)
BTerrain._setobjattrs(obj, **metadata)
# Center the mesh to its origin and create a UV map for draping
# ortho images.
BTerrain._center(obj)
return obj
@staticmethod
def reload(obj, dtm):
"""
Replaces an exisiting object's terrain mesh
This replaces an object's mesh with a new mesh, transferring old
materials over to the new mesh. This is useful for reloading DTMs
at different resolutions but maintaining textures/location/rotation.
Parameters
-----------
obj : bpy_types.Object
An already existing Blender object
dtm : DTM
Returns
----------
obj : bpy_types.Object
"""
old_mesh = obj.data
new_mesh = BTerrain._mesh_from_dtm(dtm)
# Copy any old materials to the new mesh
for mat in old_mesh.materials:
new_mesh.materials.append(mat.copy())
# Swap out the old mesh for the new one
obj.data = new_mesh
# Update out-dated meta-information
metadata = BTerrain._create_metadata(dtm)
BTerrain._setobjattrs(obj, **metadata)
# Center the mesh to its origin and create a UV map for draping
# ortho images.
BTerrain._center(obj)
return obj
@staticmethod
def _mesh_from_dtm(dtm, name='Terrain'):
"""
Creates a Blender *mesh* from a DTM
Parameters
----------
dtm : DTM
name : str, optional
The name that will be assigned to the new mesh, defaults
to 'Terrain' (and, if an object named 'Terrain' already
exists, Blender will automatically extend the name of the
new object to something like 'Terrain.001')
Returns
----------
mesh : bpy_types.Mesh
Notes
----------
* We are switching coordinate systems from the NumPy to Blender.
Numpy: Blender:
+ ----> (0, j) ^ (0, y)
| |
| |
v (i, 0) + ----> (x, 0)
"""
# Create an empty mesh
mesh = bpy.data.meshes.new(name)
# Get the xy-coordinates from the DTM, see docstring notes
y, x = np.indices(dtm.data.shape).astype('float64')
x *= dtm.mesh_scale
y *= -1 * dtm.mesh_scale
# Create an array of 3D vertices
vertices = np.dstack([x, y, dtm.data]).reshape((-1, 3))
# Drop vertices with NaN values (used in the DTM to represent
# areas with no data)
vertices = vertices[~np.isnan(vertices).any(axis=1)]
# Calculate the faces of the mesh
triangulation = Triangulate(dtm.data)
faces = triangulation.face_list()
# Fill the mesh
mesh.from_pydata(vertices, [], faces)
mesh.update()
# Create a new UV layer
mesh.uv_textures.new("HiRISE Generated UV Map")
# We'll use a bmesh to populate the UV map with values
bm = bmesh.new()
bm.from_mesh(mesh)
bm.faces.ensure_lookup_table()
uv_layer = bm.loops.layers.uv[0]
# Iterate over each face in the bmesh
num_faces = len(bm.faces)
w = dtm.data.shape[1]
h = dtm.data.shape[0]
for face_index in range(num_faces):
# Iterate over each loop in the face
for loop in bm.faces[face_index].loops:
# Get this loop's vertex coordinates
vert_coords = loop.vert.co.xy
# And calculate it's uv coordinate. We do this by dividing the
# vertice's x and y coordinates by:
#
# d + 1, dimensions of DTM (in "posts")
# mesh_scale, meters/DTM "post"
#
# This has the effect of mapping the vertex to its
# corresponding "post" index in the DTM, and then mapping
# that value to the range [0, 1).
u = vert_coords.x / ((w + 1) * dtm.mesh_scale)
v = 1 + vert_coords.y / ((h + 1) * dtm.mesh_scale)
loop[uv_layer].uv = (u, v)
bm.to_mesh(mesh)
return mesh
@staticmethod
def _center(obj):
"""Move object geometry to object origin"""
bpy.context.scene.objects.active = obj
bpy.ops.object.origin_set(center='BOUNDS')
@staticmethod
def _setobjattrs(obj, **attrs):
for key, value in attrs.items():
obj[key] = value
@staticmethod
def _create_metadata(dtm):
"""Returns a dict containing meta-information about a DTM"""
return {
'PATH': dtm.path,
'MESH_SCALE': dtm.mesh_scale,
'DTM_RESOLUTION': dtm.terrain_resolution,
'BIN_SIZE': dtm.bin_size,
'MAP_SIZE': dtm.map_size,
'MAP_SCALE': dtm.map_scale * dtm.unit_scale,
'UNIT_SCALE': dtm.unit_scale,
'IS_TERRAIN': True,
'HAS_UV_MAP': True
}
|