|
"""Primitives, conforming to the glTF 2.0 standards as specified in |
|
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-primitive |
|
|
|
Author: Matthew Matl |
|
""" |
|
import numpy as np |
|
|
|
from OpenGL.GL import * |
|
|
|
from .material import Material, MetallicRoughnessMaterial |
|
from .constants import FLOAT_SZ, UINT_SZ, BufFlags, GLTF |
|
from .utils import format_color_array |
|
|
|
|
|
class Primitive(object): |
|
"""A primitive object which can be rendered. |
|
|
|
Parameters |
|
---------- |
|
positions : (n, 3) float |
|
XYZ vertex positions. |
|
normals : (n, 3) float |
|
Normalized XYZ vertex normals. |
|
tangents : (n, 4) float |
|
XYZW vertex tangents where the w component is a sign value |
|
(either +1 or -1) indicating the handedness of the tangent basis. |
|
texcoord_0 : (n, 2) float |
|
The first set of UV texture coordinates. |
|
texcoord_1 : (n, 2) float |
|
The second set of UV texture coordinates. |
|
color_0 : (n, 4) float |
|
RGBA vertex colors. |
|
joints_0 : (n, 4) float |
|
Joint information. |
|
weights_0 : (n, 4) float |
|
Weight information for morphing. |
|
indices : (m, 3) int |
|
Face indices for triangle meshes or fans. |
|
material : :class:`Material` |
|
The material to apply to this primitive when rendering. |
|
mode : int |
|
The type of primitives to render, one of the following: |
|
|
|
- ``0``: POINTS |
|
- ``1``: LINES |
|
- ``2``: LINE_LOOP |
|
- ``3``: LINE_STRIP |
|
- ``4``: TRIANGLES |
|
- ``5``: TRIANGLES_STRIP |
|
- ``6``: TRIANGLES_FAN |
|
targets : (k,) int |
|
Morph target indices. |
|
poses : (x,4,4), float |
|
Array of 4x4 transformation matrices for instancing this object. |
|
""" |
|
|
|
def __init__(self, |
|
positions, |
|
normals=None, |
|
tangents=None, |
|
texcoord_0=None, |
|
texcoord_1=None, |
|
color_0=None, |
|
joints_0=None, |
|
weights_0=None, |
|
indices=None, |
|
material=None, |
|
mode=None, |
|
targets=None, |
|
poses=None): |
|
|
|
if mode is None: |
|
mode = GLTF.TRIANGLES |
|
|
|
self.positions = positions |
|
self.normals = normals |
|
self.tangents = tangents |
|
self.texcoord_0 = texcoord_0 |
|
self.texcoord_1 = texcoord_1 |
|
self.color_0 = color_0 |
|
self.joints_0 = joints_0 |
|
self.weights_0 = weights_0 |
|
self.indices = indices |
|
self.material = material |
|
self.mode = mode |
|
self.targets = targets |
|
self.poses = poses |
|
|
|
self._bounds = None |
|
self._vaid = None |
|
self._buffers = [] |
|
self._is_transparent = None |
|
self._buf_flags = None |
|
|
|
@property |
|
def positions(self): |
|
"""(n,3) float : XYZ vertex positions. |
|
""" |
|
return self._positions |
|
|
|
@positions.setter |
|
def positions(self, value): |
|
value = np.asanyarray(value, dtype=np.float32) |
|
self._positions = np.ascontiguousarray(value) |
|
self._bounds = None |
|
|
|
@property |
|
def normals(self): |
|
"""(n,3) float : Normalized XYZ vertex normals. |
|
""" |
|
return self._normals |
|
|
|
@normals.setter |
|
def normals(self, value): |
|
if value is not None: |
|
value = np.asanyarray(value, dtype=np.float32) |
|
value = np.ascontiguousarray(value) |
|
if value.shape != self.positions.shape: |
|
raise ValueError('Incorrect normals shape') |
|
self._normals = value |
|
|
|
@property |
|
def tangents(self): |
|
"""(n,4) float : XYZW vertex tangents. |
|
""" |
|
return self._tangents |
|
|
|
@tangents.setter |
|
def tangents(self, value): |
|
if value is not None: |
|
value = np.asanyarray(value, dtype=np.float32) |
|
value = np.ascontiguousarray(value) |
|
if value.shape != (self.positions.shape[0], 4): |
|
raise ValueError('Incorrect tangent shape') |
|
self._tangents = value |
|
|
|
@property |
|
def texcoord_0(self): |
|
"""(n,2) float : The first set of UV texture coordinates. |
|
""" |
|
return self._texcoord_0 |
|
|
|
@texcoord_0.setter |
|
def texcoord_0(self, value): |
|
if value is not None: |
|
value = np.asanyarray(value, dtype=np.float32) |
|
value = np.ascontiguousarray(value) |
|
if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or |
|
value.shape[1] < 2): |
|
raise ValueError('Incorrect texture coordinate shape') |
|
if value.shape[1] > 2: |
|
value = value[:,:2] |
|
self._texcoord_0 = value |
|
|
|
@property |
|
def texcoord_1(self): |
|
"""(n,2) float : The second set of UV texture coordinates. |
|
""" |
|
return self._texcoord_1 |
|
|
|
@texcoord_1.setter |
|
def texcoord_1(self, value): |
|
if value is not None: |
|
value = np.asanyarray(value, dtype=np.float32) |
|
value = np.ascontiguousarray(value) |
|
if (value.ndim != 2 or value.shape[0] != self.positions.shape[0] or |
|
value.shape[1] != 2): |
|
raise ValueError('Incorrect texture coordinate shape') |
|
self._texcoord_1 = value |
|
|
|
@property |
|
def color_0(self): |
|
"""(n,4) float : RGBA vertex colors. |
|
""" |
|
return self._color_0 |
|
|
|
@color_0.setter |
|
def color_0(self, value): |
|
if value is not None: |
|
value = np.ascontiguousarray( |
|
format_color_array(value, shape=(len(self.positions), 4)) |
|
) |
|
self._is_transparent = None |
|
self._color_0 = value |
|
|
|
@property |
|
def joints_0(self): |
|
"""(n,4) float : Joint information. |
|
""" |
|
return self._joints_0 |
|
|
|
@joints_0.setter |
|
def joints_0(self, value): |
|
self._joints_0 = value |
|
|
|
@property |
|
def weights_0(self): |
|
"""(n,4) float : Weight information for morphing. |
|
""" |
|
return self._weights_0 |
|
|
|
@weights_0.setter |
|
def weights_0(self, value): |
|
self._weights_0 = value |
|
|
|
@property |
|
def indices(self): |
|
"""(m,3) int : Face indices for triangle meshes or fans. |
|
""" |
|
return self._indices |
|
|
|
@indices.setter |
|
def indices(self, value): |
|
if value is not None: |
|
value = np.asanyarray(value, dtype=np.float32) |
|
value = np.ascontiguousarray(value) |
|
self._indices = value |
|
|
|
@property |
|
def material(self): |
|
""":class:`Material` : The material for this primitive. |
|
""" |
|
return self._material |
|
|
|
@material.setter |
|
def material(self, value): |
|
|
|
if value is None: |
|
value = MetallicRoughnessMaterial() |
|
else: |
|
if not isinstance(value, Material): |
|
raise TypeError('Object material must be of type Material') |
|
self._material = value |
|
|
|
@property |
|
def mode(self): |
|
"""int : The type of primitive to render. |
|
""" |
|
return self._mode |
|
|
|
@mode.setter |
|
def mode(self, value): |
|
value = int(value) |
|
if value < GLTF.POINTS or value > GLTF.TRIANGLE_FAN: |
|
raise ValueError('Invalid mode') |
|
self._mode = value |
|
|
|
@property |
|
def targets(self): |
|
"""(k,) int : Morph target indices. |
|
""" |
|
return self._targets |
|
|
|
@targets.setter |
|
def targets(self, value): |
|
self._targets = value |
|
|
|
@property |
|
def poses(self): |
|
"""(x,4,4) float : Homogenous transforms for instancing this primitive. |
|
""" |
|
return self._poses |
|
|
|
@poses.setter |
|
def poses(self, value): |
|
if value is not None: |
|
value = np.asanyarray(value, dtype=np.float32) |
|
value = np.ascontiguousarray(value) |
|
if value.ndim == 2: |
|
value = value[np.newaxis,:,:] |
|
if value.shape[1] != 4 or value.shape[2] != 4: |
|
raise ValueError('Pose matrices must be of shape (n,4,4), ' |
|
'got {}'.format(value.shape)) |
|
self._poses = value |
|
self._bounds = None |
|
|
|
@property |
|
def bounds(self): |
|
if self._bounds is None: |
|
self._bounds = self._compute_bounds() |
|
return self._bounds |
|
|
|
@property |
|
def centroid(self): |
|
"""(3,) float : The centroid of the primitive's AABB. |
|
""" |
|
return np.mean(self.bounds, axis=0) |
|
|
|
@property |
|
def extents(self): |
|
"""(3,) float : The lengths of the axes of the primitive's AABB. |
|
""" |
|
return np.diff(self.bounds, axis=0).reshape(-1) |
|
|
|
@property |
|
def scale(self): |
|
"""(3,) float : The length of the diagonal of the primitive's AABB. |
|
""" |
|
return np.linalg.norm(self.extents) |
|
|
|
@property |
|
def buf_flags(self): |
|
"""int : The flags for the render buffer. |
|
""" |
|
if self._buf_flags is None: |
|
self._buf_flags = self._compute_buf_flags() |
|
return self._buf_flags |
|
|
|
def delete(self): |
|
self._unbind() |
|
self._remove_from_context() |
|
|
|
@property |
|
def is_transparent(self): |
|
"""bool : If True, the mesh is partially-transparent. |
|
""" |
|
return self._compute_transparency() |
|
|
|
def _add_to_context(self): |
|
if self._vaid is not None: |
|
raise ValueError('Mesh is already bound to a context') |
|
|
|
|
|
self._vaid = glGenVertexArrays(1) |
|
glBindVertexArray(self._vaid) |
|
|
|
|
|
|
|
|
|
|
|
|
|
vertexbuffer = glGenBuffers(1) |
|
self._buffers.append(vertexbuffer) |
|
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer) |
|
|
|
|
|
vertex_data = self.positions |
|
attr_sizes = [3] |
|
|
|
|
|
if self.normals is not None: |
|
vertex_data = np.hstack((vertex_data, self.normals)) |
|
attr_sizes.append(3) |
|
|
|
|
|
if self.tangents is not None: |
|
vertex_data = np.hstack((vertex_data, self.tangents)) |
|
attr_sizes.append(4) |
|
|
|
|
|
if self.texcoord_0 is not None: |
|
vertex_data = np.hstack((vertex_data, self.texcoord_0)) |
|
attr_sizes.append(2) |
|
if self.texcoord_1 is not None: |
|
vertex_data = np.hstack((vertex_data, self.texcoord_1)) |
|
attr_sizes.append(2) |
|
|
|
|
|
if self.color_0 is not None: |
|
vertex_data = np.hstack((vertex_data, self.color_0)) |
|
attr_sizes.append(4) |
|
|
|
|
|
|
|
|
|
|
|
vertex_data = np.ascontiguousarray( |
|
vertex_data.flatten().astype(np.float32) |
|
) |
|
glBufferData( |
|
GL_ARRAY_BUFFER, FLOAT_SZ * len(vertex_data), |
|
vertex_data, GL_STATIC_DRAW |
|
) |
|
total_sz = sum(attr_sizes) |
|
offset = 0 |
|
for i, sz in enumerate(attr_sizes): |
|
glVertexAttribPointer( |
|
i, sz, GL_FLOAT, GL_FALSE, FLOAT_SZ * total_sz, |
|
ctypes.c_void_p(FLOAT_SZ * offset) |
|
) |
|
glEnableVertexAttribArray(i) |
|
offset += sz |
|
|
|
|
|
|
|
|
|
|
|
if self.poses is not None: |
|
pose_data = np.ascontiguousarray( |
|
np.transpose(self.poses, [0,2,1]).flatten().astype(np.float32) |
|
) |
|
else: |
|
pose_data = np.ascontiguousarray( |
|
np.eye(4).flatten().astype(np.float32) |
|
) |
|
|
|
modelbuffer = glGenBuffers(1) |
|
self._buffers.append(modelbuffer) |
|
glBindBuffer(GL_ARRAY_BUFFER, modelbuffer) |
|
glBufferData( |
|
GL_ARRAY_BUFFER, FLOAT_SZ * len(pose_data), |
|
pose_data, GL_STATIC_DRAW |
|
) |
|
|
|
for i in range(0, 4): |
|
idx = i + len(attr_sizes) |
|
glEnableVertexAttribArray(idx) |
|
glVertexAttribPointer( |
|
idx, 4, GL_FLOAT, GL_FALSE, FLOAT_SZ * 4 * 4, |
|
ctypes.c_void_p(4 * FLOAT_SZ * i) |
|
) |
|
glVertexAttribDivisor(idx, 1) |
|
|
|
|
|
|
|
|
|
if self.indices is not None: |
|
elementbuffer = glGenBuffers(1) |
|
self._buffers.append(elementbuffer) |
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer) |
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, UINT_SZ * self.indices.size, |
|
self.indices.flatten().astype(np.uint32), |
|
GL_STATIC_DRAW) |
|
|
|
glBindVertexArray(0) |
|
|
|
def _remove_from_context(self): |
|
if self._vaid is not None: |
|
glDeleteVertexArrays(1, [self._vaid]) |
|
glDeleteBuffers(len(self._buffers), self._buffers) |
|
self._vaid = None |
|
self._buffers = [] |
|
|
|
def _in_context(self): |
|
return self._vaid is not None |
|
|
|
def _bind(self): |
|
if self._vaid is None: |
|
raise ValueError('Cannot bind a Mesh that has not been added ' |
|
'to a context') |
|
glBindVertexArray(self._vaid) |
|
|
|
def _unbind(self): |
|
glBindVertexArray(0) |
|
|
|
def _compute_bounds(self): |
|
"""Compute the bounds of this object. |
|
""" |
|
|
|
bounds = np.array([np.min(self.positions, axis=0), |
|
np.max(self.positions, axis=0)]) |
|
|
|
|
|
if self.poses is not None: |
|
bounds += np.array([np.min(self.poses[:,:3,3], axis=0), |
|
np.max(self.poses[:,:3,3], axis=0)]) |
|
return bounds |
|
|
|
def _compute_transparency(self): |
|
"""Compute whether or not this object is transparent. |
|
""" |
|
if self.material.is_transparent: |
|
return True |
|
if self._is_transparent is None: |
|
self._is_transparent = False |
|
if self.color_0 is not None: |
|
if np.any(self._color_0[:,3] != 1.0): |
|
self._is_transparent = True |
|
return self._is_transparent |
|
|
|
def _compute_buf_flags(self): |
|
buf_flags = BufFlags.POSITION |
|
|
|
if self.normals is not None: |
|
buf_flags |= BufFlags.NORMAL |
|
if self.tangents is not None: |
|
buf_flags |= BufFlags.TANGENT |
|
if self.texcoord_0 is not None: |
|
buf_flags |= BufFlags.TEXCOORD_0 |
|
if self.texcoord_1 is not None: |
|
buf_flags |= BufFlags.TEXCOORD_1 |
|
if self.color_0 is not None: |
|
buf_flags |= BufFlags.COLOR_0 |
|
if self.joints_0 is not None: |
|
buf_flags |= BufFlags.JOINTS_0 |
|
if self.weights_0 is not None: |
|
buf_flags |= BufFlags.WEIGHTS_0 |
|
|
|
return buf_flags |
|
|