Welcome to mirror list, hosted at ThFree Co, Russian Federation.

terrain.py « mesh « io_convert_image_to_mesh_img - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: db866289af686811d65efaa08f6cec16047ce6ec (plain)
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
        }