new stuff
This commit is contained in:
parent
8998bc3936
commit
64c8492e5c
6907 changed files with 3401899 additions and 0 deletions
574
assimp/port/PyAssimp/pyassimp/core.py
Normal file
574
assimp/port/PyAssimp/pyassimp/core.py
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
"""
|
||||
PyAssimp
|
||||
|
||||
This is the main-module of PyAssimp.
|
||||
"""
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2,6):
|
||||
raise RuntimeError('pyassimp: need python 2.6 or newer')
|
||||
|
||||
# xrange was renamed range in Python 3 and the original range from Python 2 was removed.
|
||||
# To keep compatibility with both Python 2 and 3, xrange is set to range for version 3.0 and up.
|
||||
if sys.version_info >= (3,0):
|
||||
xrange = range
|
||||
|
||||
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
numpy = None
|
||||
import logging
|
||||
import ctypes
|
||||
from contextlib import contextmanager
|
||||
logger = logging.getLogger("pyassimp")
|
||||
# attach default null handler to logger so it doesn't complain
|
||||
# even if you don't attach another handler to logger
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
from . import structs
|
||||
from . import helper
|
||||
from . import postprocess
|
||||
from .errors import AssimpError
|
||||
|
||||
class AssimpLib(object):
|
||||
"""
|
||||
Assimp-Singleton
|
||||
"""
|
||||
load, load_mem, export, export_blob, release, dll = helper.search_library()
|
||||
_assimp_lib = AssimpLib()
|
||||
|
||||
def make_tuple(ai_obj, type = None):
|
||||
res = None
|
||||
|
||||
#notes:
|
||||
# ai_obj._fields_ = [ ("attr", c_type), ... ]
|
||||
# getattr(ai_obj, e[0]).__class__ == float
|
||||
|
||||
if isinstance(ai_obj, structs.Matrix4x4):
|
||||
if numpy:
|
||||
res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_]).reshape((4,4))
|
||||
#import pdb;pdb.set_trace()
|
||||
else:
|
||||
res = [getattr(ai_obj, e[0]) for e in ai_obj._fields_]
|
||||
res = [res[i:i+4] for i in xrange(0,16,4)]
|
||||
elif isinstance(ai_obj, structs.Matrix3x3):
|
||||
if numpy:
|
||||
res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_]).reshape((3,3))
|
||||
else:
|
||||
res = [getattr(ai_obj, e[0]) for e in ai_obj._fields_]
|
||||
res = [res[i:i+3] for i in xrange(0,9,3)]
|
||||
else:
|
||||
if numpy:
|
||||
res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_])
|
||||
else:
|
||||
res = [getattr(ai_obj, e[0]) for e in ai_obj._fields_]
|
||||
|
||||
return res
|
||||
|
||||
# Returns unicode object for Python 2, and str object for Python 3.
|
||||
def _convert_assimp_string(assimp_string):
|
||||
if sys.version_info >= (3, 0):
|
||||
return str(assimp_string.data, errors='ignore')
|
||||
else:
|
||||
return unicode(assimp_string.data, errors='ignore')
|
||||
|
||||
# It is faster and more correct to have an init function for each assimp class
|
||||
def _init_face(aiFace):
|
||||
aiFace.indices = [aiFace.mIndices[i] for i in range(aiFace.mNumIndices)]
|
||||
assimp_struct_inits = { structs.Face : _init_face }
|
||||
|
||||
def call_init(obj, caller = None):
|
||||
if helper.hasattr_silent(obj,'contents'): #pointer
|
||||
_init(obj.contents, obj, caller)
|
||||
else:
|
||||
_init(obj,parent=caller)
|
||||
|
||||
def _is_init_type(obj):
|
||||
|
||||
if obj and helper.hasattr_silent(obj,'contents'): #pointer
|
||||
return _is_init_type(obj[0])
|
||||
# null-pointer case that arises when we reach a mesh attribute
|
||||
# like mBitangents which use mNumVertices rather than mNumBitangents
|
||||
# so it breaks the 'is iterable' check.
|
||||
# Basically:
|
||||
# FIXME!
|
||||
elif not bool(obj):
|
||||
return False
|
||||
tname = obj.__class__.__name__
|
||||
return not (tname[:2] == 'c_' or tname == 'Structure' \
|
||||
or tname == 'POINTER') and not isinstance(obj, (int, str, bytes))
|
||||
|
||||
def _init(self, target = None, parent = None):
|
||||
"""
|
||||
Custom initialize() for C structs, adds safely accessible member functionality.
|
||||
|
||||
:param target: set the object which receive the added methods. Useful when manipulating
|
||||
pointers, to skip the intermediate 'contents' deferencing.
|
||||
"""
|
||||
if not target:
|
||||
target = self
|
||||
|
||||
dirself = dir(self)
|
||||
for m in dirself:
|
||||
|
||||
if m.startswith("_"):
|
||||
continue
|
||||
|
||||
# We should not be accessing `mPrivate` according to structs.Scene.
|
||||
if m == 'mPrivate':
|
||||
continue
|
||||
|
||||
if m.startswith('mNum'):
|
||||
if 'm' + m[4:] in dirself:
|
||||
continue # will be processed later on
|
||||
else:
|
||||
name = m[1:].lower()
|
||||
|
||||
obj = getattr(self, m)
|
||||
setattr(target, name, obj)
|
||||
continue
|
||||
|
||||
if m == 'mName':
|
||||
target.name = str(_convert_assimp_string(self.mName))
|
||||
target.__class__.__repr__ = lambda x: str(x.__class__) + "(" + getattr(x, 'name','') + ")"
|
||||
target.__class__.__str__ = lambda x: getattr(x, 'name', '')
|
||||
continue
|
||||
|
||||
name = m[1:].lower()
|
||||
|
||||
obj = getattr(self, m)
|
||||
|
||||
# Create tuples
|
||||
if isinstance(obj, structs.assimp_structs_as_tuple):
|
||||
setattr(target, name, make_tuple(obj))
|
||||
logger.debug(str(self) + ": Added array " + str(getattr(target, name)) + " as self." + name.lower())
|
||||
continue
|
||||
|
||||
if m.startswith('m') and len(m) > 1 and m[1].upper() == m[1]:
|
||||
|
||||
if name == "parent":
|
||||
setattr(target, name, parent)
|
||||
logger.debug("Added a parent as self." + name)
|
||||
continue
|
||||
|
||||
if helper.hasattr_silent(self, 'mNum' + m[1:]):
|
||||
|
||||
length = getattr(self, 'mNum' + m[1:])
|
||||
|
||||
# -> special case: properties are
|
||||
# stored as a dict.
|
||||
if m == 'mProperties':
|
||||
setattr(target, name, _get_properties(obj, length))
|
||||
continue
|
||||
|
||||
|
||||
if not length: # empty!
|
||||
setattr(target, name, [])
|
||||
logger.debug(str(self) + ": " + name + " is an empty list.")
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
if obj._type_ in structs.assimp_structs_as_tuple:
|
||||
if numpy:
|
||||
setattr(target, name, numpy.array([make_tuple(obj[i]) for i in range(length)], dtype=numpy.float32))
|
||||
|
||||
logger.debug(str(self) + ": Added an array of numpy arrays (type "+ str(type(obj)) + ") as self." + name)
|
||||
else:
|
||||
setattr(target, name, [make_tuple(obj[i]) for i in range(length)])
|
||||
|
||||
logger.debug(str(self) + ": Added a list of lists (type "+ str(type(obj)) + ") as self." + name)
|
||||
|
||||
else:
|
||||
setattr(target, name, [obj[i] for i in range(length)]) #TODO: maybe not necessary to recreate an array?
|
||||
|
||||
logger.debug(str(self) + ": Added list of " + str(obj) + " " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
|
||||
|
||||
# initialize array elements
|
||||
try:
|
||||
init = assimp_struct_inits[type(obj[0])]
|
||||
except KeyError:
|
||||
if _is_init_type(obj[0]):
|
||||
for e in getattr(target, name):
|
||||
call_init(e, target)
|
||||
else:
|
||||
for e in getattr(target, name):
|
||||
init(e)
|
||||
|
||||
|
||||
except IndexError:
|
||||
logger.error("in " + str(self) +" : mismatch between mNum" + name + " and the actual amount of data in m" + name + ". This may be due to version mismatch between libassimp and pyassimp. Quitting now.")
|
||||
sys.exit(1)
|
||||
|
||||
except ValueError as e:
|
||||
|
||||
logger.error("In " + str(self) + "->" + name + ": " + str(e) + ". Quitting now.")
|
||||
if "setting an array element with a sequence" in str(e):
|
||||
logger.error("Note that pyassimp does not currently "
|
||||
"support meshes with mixed triangles "
|
||||
"and quads. Try to load your mesh with"
|
||||
" a post-processing to triangulate your"
|
||||
" faces.")
|
||||
raise e
|
||||
|
||||
|
||||
|
||||
else: # starts with 'm' but not iterable
|
||||
setattr(target, name, obj)
|
||||
logger.debug("Added " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
|
||||
|
||||
if _is_init_type(obj):
|
||||
call_init(obj, target)
|
||||
|
||||
if isinstance(self, structs.Mesh):
|
||||
_finalize_mesh(self, target)
|
||||
|
||||
if isinstance(self, structs.Texture):
|
||||
_finalize_texture(self, target)
|
||||
|
||||
if isinstance(self, structs.Metadata):
|
||||
_finalize_metadata(self, target)
|
||||
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def pythonize_assimp(type, obj, scene):
|
||||
""" This method modify the Assimp data structures
|
||||
to make them easier to work with in Python.
|
||||
|
||||
Supported operations:
|
||||
- MESH: replace a list of mesh IDs by reference to these meshes
|
||||
- ADDTRANSFORMATION: add a reference to an object's transformation taken from their associated node.
|
||||
|
||||
:param type: the type of modification to operate (cf above)
|
||||
:param obj: the input object to modify
|
||||
:param scene: a reference to the whole scene
|
||||
"""
|
||||
|
||||
if type == "MESH":
|
||||
meshes = []
|
||||
for i in obj:
|
||||
meshes.append(scene.meshes[i])
|
||||
return meshes
|
||||
|
||||
if type == "ADDTRANSFORMATION":
|
||||
def getnode(node, name):
|
||||
if node.name == name: return node
|
||||
for child in node.children:
|
||||
n = getnode(child, name)
|
||||
if n: return n
|
||||
|
||||
node = getnode(scene.rootnode, obj.name)
|
||||
if not node:
|
||||
raise AssimpError("Object " + str(obj) + " has no associated node!")
|
||||
setattr(obj, "transformation", node.transformation)
|
||||
|
||||
def recur_pythonize(node, scene):
|
||||
'''
|
||||
Recursively call pythonize_assimp on
|
||||
nodes tree to apply several post-processing to
|
||||
pythonize the assimp datastructures.
|
||||
'''
|
||||
node.meshes = pythonize_assimp("MESH", node.meshes, scene)
|
||||
for mesh in node.meshes:
|
||||
mesh.material = scene.materials[mesh.materialindex]
|
||||
for cam in scene.cameras:
|
||||
pythonize_assimp("ADDTRANSFORMATION", cam, scene)
|
||||
for c in node.children:
|
||||
recur_pythonize(c, scene)
|
||||
|
||||
def release(scene):
|
||||
'''
|
||||
Release resources of a loaded scene.
|
||||
'''
|
||||
_assimp_lib.release(ctypes.pointer(scene))
|
||||
|
||||
@contextmanager
|
||||
def load(filename,
|
||||
file_type = None,
|
||||
processing = postprocess.aiProcess_Triangulate):
|
||||
'''
|
||||
Load a model into a scene. On failure throws AssimpError.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
filename: Either a filename or a file object to load model from.
|
||||
If a file object is passed, file_type MUST be specified
|
||||
Otherwise Assimp has no idea which importer to use.
|
||||
This is named 'filename' so as to not break legacy code.
|
||||
processing: assimp postprocessing parameters. Verbose keywords are imported
|
||||
from postprocessing, and the parameters can be combined bitwise to
|
||||
generate the final processing value. Note that the default value will
|
||||
triangulate quad faces. Example of generating other possible values:
|
||||
processing = (pyassimp.postprocess.aiProcess_Triangulate |
|
||||
pyassimp.postprocess.aiProcess_OptimizeMeshes)
|
||||
file_type: string of file extension, such as 'stl'
|
||||
|
||||
Returns
|
||||
---------
|
||||
Scene object with model data
|
||||
'''
|
||||
|
||||
from ctypes import c_char_p
|
||||
if hasattr(filename, 'read'):
|
||||
# This is the case where a file object has been passed to load.
|
||||
# It is calling the following function:
|
||||
# const aiScene* aiImportFileFromMemory(const char* pBuffer,
|
||||
# unsigned int pLength,
|
||||
# unsigned int pFlags,
|
||||
# const char* pHint)
|
||||
if file_type is None:
|
||||
raise AssimpError('File type must be specified when passing file objects!')
|
||||
data = filename.read()
|
||||
model = _assimp_lib.load_mem(data,
|
||||
len(data),
|
||||
processing,
|
||||
c_char_p(file_type.encode(sys.getfilesystemencoding())))
|
||||
else:
|
||||
# a filename string has been passed
|
||||
model = _assimp_lib.load(filename.encode(sys.getfilesystemencoding()), processing)
|
||||
|
||||
if not model:
|
||||
raise AssimpError('Could not import file!')
|
||||
scene = _init(model.contents)
|
||||
recur_pythonize(scene.rootnode, scene)
|
||||
try:
|
||||
yield scene
|
||||
finally:
|
||||
release(scene)
|
||||
|
||||
def export(scene,
|
||||
filename,
|
||||
file_type = None,
|
||||
processing = postprocess.aiProcess_Triangulate):
|
||||
'''
|
||||
Export a scene. On failure throws AssimpError.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
scene: scene to export.
|
||||
filename: Filename that the scene should be exported to.
|
||||
file_type: string of file exporter to use. For example "collada".
|
||||
processing: assimp postprocessing parameters. Verbose keywords are imported
|
||||
from postprocessing, and the parameters can be combined bitwise to
|
||||
generate the final processing value. Note that the default value will
|
||||
triangulate quad faces. Example of generating other possible values:
|
||||
processing = (pyassimp.postprocess.aiProcess_Triangulate |
|
||||
pyassimp.postprocess.aiProcess_OptimizeMeshes)
|
||||
|
||||
'''
|
||||
|
||||
exportStatus = _assimp_lib.export(ctypes.pointer(scene), file_type.encode("ascii"), filename.encode(sys.getfilesystemencoding()), processing)
|
||||
|
||||
if exportStatus != 0:
|
||||
raise AssimpError('Could not export scene!')
|
||||
|
||||
def export_blob(scene,
|
||||
file_type = None,
|
||||
processing = postprocess.aiProcess_Triangulate):
|
||||
'''
|
||||
Export a scene and return a blob in the correct format. On failure throws AssimpError.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
scene: scene to export.
|
||||
file_type: string of file exporter to use. For example "collada".
|
||||
processing: assimp postprocessing parameters. Verbose keywords are imported
|
||||
from postprocessing, and the parameters can be combined bitwise to
|
||||
generate the final processing value. Note that the default value will
|
||||
triangulate quad faces. Example of generating other possible values:
|
||||
processing = (pyassimp.postprocess.aiProcess_Triangulate |
|
||||
pyassimp.postprocess.aiProcess_OptimizeMeshes)
|
||||
Returns
|
||||
---------
|
||||
Pointer to structs.ExportDataBlob
|
||||
'''
|
||||
exportBlobPtr = _assimp_lib.export_blob(ctypes.pointer(scene), file_type.encode("ascii"), processing)
|
||||
|
||||
if exportBlobPtr == 0:
|
||||
raise AssimpError('Could not export scene to blob!')
|
||||
return exportBlobPtr
|
||||
|
||||
def available_formats():
|
||||
"""
|
||||
Return a list of file format extensions supported to import.
|
||||
|
||||
Returns
|
||||
---------
|
||||
A list of upper-case file extensions, e.g. [3DS, OBJ]
|
||||
"""
|
||||
from ctypes import byref
|
||||
extension_list = structs.String()
|
||||
_assimp_lib.dll.aiGetExtensionList(byref(extension_list))
|
||||
return [e[2:].upper() for e in str(extension_list.data, sys.getfilesystemencoding()).split(";")]
|
||||
|
||||
def _finalize_texture(tex, target):
|
||||
setattr(target, "achformathint", tex.achFormatHint)
|
||||
if numpy:
|
||||
data = numpy.array([make_tuple(getattr(tex, "pcData")[i]) for i in range(tex.mWidth * tex.mHeight)])
|
||||
else:
|
||||
data = [make_tuple(getattr(tex, "pcData")[i]) for i in range(tex.mWidth * tex.mHeight)]
|
||||
setattr(target, "data", data)
|
||||
|
||||
def _finalize_mesh(mesh, target):
|
||||
""" Building of meshes is a bit specific.
|
||||
|
||||
We override here the various datasets that can
|
||||
not be process as regular fields.
|
||||
|
||||
For instance, the length of the normals array is
|
||||
mNumVertices (no mNumNormals is available)
|
||||
"""
|
||||
nb_vertices = getattr(mesh, "mNumVertices")
|
||||
|
||||
def fill(name):
|
||||
mAttr = getattr(mesh, name)
|
||||
if numpy:
|
||||
if mAttr:
|
||||
data = numpy.array([make_tuple(getattr(mesh, name)[i]) for i in range(nb_vertices)], dtype=numpy.float32)
|
||||
setattr(target, name[1:].lower(), data)
|
||||
else:
|
||||
setattr(target, name[1:].lower(), numpy.array([], dtype="float32"))
|
||||
else:
|
||||
if mAttr:
|
||||
data = [make_tuple(getattr(mesh, name)[i]) for i in range(nb_vertices)]
|
||||
setattr(target, name[1:].lower(), data)
|
||||
else:
|
||||
setattr(target, name[1:].lower(), [])
|
||||
|
||||
def fillarray(name):
|
||||
mAttr = getattr(mesh, name)
|
||||
|
||||
data = []
|
||||
for index, mSubAttr in enumerate(mAttr):
|
||||
if mSubAttr:
|
||||
data.append([make_tuple(getattr(mesh, name)[index][i]) for i in range(nb_vertices)])
|
||||
|
||||
if numpy:
|
||||
setattr(target, name[1:].lower(), numpy.array(data, dtype=numpy.float32))
|
||||
else:
|
||||
setattr(target, name[1:].lower(), data)
|
||||
|
||||
fill("mNormals")
|
||||
fill("mTangents")
|
||||
fill("mBitangents")
|
||||
|
||||
fillarray("mColors")
|
||||
fillarray("mTextureCoords")
|
||||
|
||||
# prepare faces
|
||||
if numpy:
|
||||
faces = numpy.array([f.indices for f in target.faces], dtype=numpy.int32)
|
||||
else:
|
||||
faces = [f.indices for f in target.faces]
|
||||
setattr(target, 'faces', faces)
|
||||
|
||||
def _init_metadata_entry(entry):
|
||||
entry.type = entry.mType
|
||||
if entry.type == structs.MetadataEntry.AI_BOOL:
|
||||
entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_bool)).contents.value
|
||||
elif entry.type == structs.MetadataEntry.AI_INT32:
|
||||
entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_int32)).contents.value
|
||||
elif entry.type == structs.MetadataEntry.AI_UINT64:
|
||||
entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_uint64)).contents.value
|
||||
elif entry.type == structs.MetadataEntry.AI_FLOAT:
|
||||
entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_float)).contents.value
|
||||
elif entry.type == structs.MetadataEntry.AI_DOUBLE:
|
||||
entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_double)).contents.value
|
||||
elif entry.type == structs.MetadataEntry.AI_AISTRING:
|
||||
assimp_string = ctypes.cast(entry.mData, ctypes.POINTER(structs.String)).contents
|
||||
entry.data = _convert_assimp_string(assimp_string)
|
||||
elif entry.type == structs.MetadataEntry.AI_AIVECTOR3D:
|
||||
assimp_vector = ctypes.cast(entry.mData, ctypes.POINTER(structs.Vector3D)).contents
|
||||
entry.data = make_tuple(assimp_vector)
|
||||
|
||||
return entry
|
||||
|
||||
def _finalize_metadata(metadata, target):
|
||||
""" Building the metadata object is a bit specific.
|
||||
|
||||
Firstly, there are two separate arrays: one with metadata keys and one
|
||||
with metadata values, and there are no corresponding mNum* attributes,
|
||||
so the C arrays are not converted to Python arrays using the generic
|
||||
code in the _init function.
|
||||
|
||||
Secondly, a metadata entry value has to be cast according to declared
|
||||
metadata entry type.
|
||||
"""
|
||||
length = metadata.mNumProperties
|
||||
setattr(target, 'keys', [str(_convert_assimp_string(metadata.mKeys[i])) for i in range(length)])
|
||||
setattr(target, 'values', [_init_metadata_entry(metadata.mValues[i]) for i in range(length)])
|
||||
|
||||
class PropertyGetter(dict):
|
||||
def __getitem__(self, key):
|
||||
semantic = 0
|
||||
if isinstance(key, tuple):
|
||||
key, semantic = key
|
||||
|
||||
return dict.__getitem__(self, (key, semantic))
|
||||
|
||||
def keys(self):
|
||||
for k in dict.keys(self):
|
||||
yield k[0]
|
||||
|
||||
def __iter__(self):
|
||||
return self.keys()
|
||||
|
||||
def items(self):
|
||||
for k, v in dict.items(self):
|
||||
yield k[0], v
|
||||
|
||||
|
||||
def _get_properties(properties, length):
|
||||
"""
|
||||
Convenience Function to get the material properties as a dict
|
||||
and values in a python format.
|
||||
"""
|
||||
result = {}
|
||||
#read all properties
|
||||
for p in [properties[i] for i in range(length)]:
|
||||
#the name
|
||||
p = p.contents
|
||||
key = str(_convert_assimp_string(p.mKey))
|
||||
key = (key.split('.')[1], p.mSemantic)
|
||||
|
||||
#the data
|
||||
if p.mType == 1:
|
||||
arr = ctypes.cast(p.mData,
|
||||
ctypes.POINTER(ctypes.c_float * int(p.mDataLength/ctypes.sizeof(ctypes.c_float)))
|
||||
).contents
|
||||
value = [x for x in arr]
|
||||
elif p.mType == 3: #string can't be an array
|
||||
value = _convert_assimp_string(ctypes.cast(p.mData, ctypes.POINTER(structs.MaterialPropertyString)).contents)
|
||||
|
||||
elif p.mType == 4:
|
||||
arr = ctypes.cast(p.mData,
|
||||
ctypes.POINTER(ctypes.c_int * int(p.mDataLength/ctypes.sizeof(ctypes.c_int)))
|
||||
).contents
|
||||
value = [x for x in arr]
|
||||
else:
|
||||
value = p.mData[:p.mDataLength]
|
||||
|
||||
if len(value) == 1:
|
||||
[value] = value
|
||||
|
||||
result[key] = value
|
||||
|
||||
return PropertyGetter(result)
|
||||
|
||||
def decompose_matrix(matrix):
|
||||
if not isinstance(matrix, structs.Matrix4x4):
|
||||
raise AssimpError("pyassimp.decompose_matrix failed: Not a Matrix4x4!")
|
||||
|
||||
scaling = structs.Vector3D()
|
||||
rotation = structs.Quaternion()
|
||||
position = structs.Vector3D()
|
||||
|
||||
_assimp_lib.dll.aiDecomposeMatrix(ctypes.pointer(matrix),
|
||||
ctypes.byref(scaling),
|
||||
ctypes.byref(rotation),
|
||||
ctypes.byref(position))
|
||||
return scaling._init(), rotation._init(), position._init()
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue