├── .gitignore ├── screenshot.png ├── background └── README.md ├── math3d.py ├── TODO.md ├── COPYING ├── glfw_platform.py ├── README.md ├── parse_bg.py ├── visualize.py ├── transformations.py └── doc └── HOD_file_format.html /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.pyc 3 | 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laanwj/hw2view/HEAD/screenshot.png -------------------------------------------------------------------------------- /background/README.md: -------------------------------------------------------------------------------- 1 | The .hod files are owned by Sierra / Relic Entertainment and cannot 2 | be distributed in this repository. 3 | 4 | See the toplevel README.md for instructions on how to extract 5 | the .hod files from the Homeworld 2 game. 6 | 7 | -------------------------------------------------------------------------------- /math3d.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy 3 | 4 | def perspective_matrix(fovy, aspect, near, far): 5 | ''' 6 | Build a perspective matrix (implements gluPerspective). 7 | ''' 8 | # Based on https://stackoverflow.com/questions/71807942/opengl-gluperspective-implementation 9 | mat = numpy.zeros((4, 4)) 10 | f = math.tan(math.radians(fovy) / 2.0) 11 | mat[0][0] = 1.0 / (f * aspect) 12 | mat[1][1] = 1.0 / f 13 | mat[2][2] = -(near + far) / (near - far) 14 | mat[3][2] = -(2.0 * near * far) / (near - far) 15 | mat[2][3] = -1.0 16 | return mat 17 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Port to GLES2 2 | pogles 3 | https://pypi.python.org/pypi/pogles/1.0 4 | pogles is a Python package that implements bindings for OpenGL ES v2.0 for Python v2.3 or later and Python v3. It comprises three modules: 5 | 6 | Also: https://code.google.com/p/pyglesv2/ 7 | 8 | - Can we extract directly from the Homeworld2.big file? 9 | 10 | - Set/unset automatic rotation 11 | Implement trackball rotation 12 | Set automatic rotation speed 13 | 14 | - Switch to next background automatically? 15 | 16 | - Optimizations 17 | - Number of bytes per vertex can be trivially brought down to 16 by using float3 vertices 18 | 19 | - There are 16 'submeshes', could determine which ones are visible using bounding boxes 20 | This may or may not save 21 | 22 | - Make FoV configurable 23 | I don't know what FoV the original homeworld 2 had 24 | 45 degrees vertical FoV may be too narrow whereas 90 seems to be too wide 25 | 26 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2022 W. J. van der Laan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /glfw_platform.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Minimal GLFW platform for PyOpenGL. This looks up functions through GLFW 3 | without any knowledge of the underlying platform. 4 | ''' 5 | import ctypes 6 | import glfw 7 | from OpenGL.platform import baseplatform 8 | 9 | class GLFWPlatform(baseplatform.BasePlatform): 10 | """GLFW platform""" 11 | GLES1 = None 12 | GLES2 = None 13 | GLES3 = None 14 | GL = None 15 | OpenGL = None 16 | # GLU, GLUT, GLE cannot be looked up through this platform. 17 | 18 | DEFAULT_FUNCTION_TYPE = staticmethod(ctypes.CFUNCTYPE) 19 | 20 | def constructFunction(self, 21 | functionName, dll, 22 | resultType=ctypes.c_int, argTypes=(), 23 | doc = None, argNames = (), 24 | extension = None, 25 | deprecated = False, 26 | module = None, 27 | force_extension = False, 28 | error_checker = None, 29 | ): 30 | return super().constructFunction( 31 | functionName, dll, 32 | resultType, argTypes, 33 | doc, argNames, 34 | extension, 35 | deprecated, 36 | module, 37 | True, # Force lookup through getExtensionProcedure instead of cdll 38 | None) 39 | 40 | def getExtensionProcedure(self, procname): 41 | ''' 42 | Look up function pointer for client API function. 43 | ''' 44 | return glfw.get_proc_address(procname.decode()) 45 | 46 | def GetCurrentContext(self): 47 | ''' 48 | Return context. Must be a hashable value, so take the address. 49 | ''' 50 | return ctypes.cast(glfw.get_current_context(), ctypes.c_void_p).value 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](screenshot.png) 2 | 3 | ## Description 4 | 5 | This tool can be used to view the space backgrounds from the game Homeworld 2. 6 | 7 | What makes backgrounds these special from a technical viewpoint is that they are not 8 | background or cubemap textures but tesselated spheres with vertex colors. The geometry 9 | is more finely tesselated in places where more precision is needed in the image. 10 | 11 | For further information on the way that the Homeworld 2 backgrounds work 12 | please refer to these Simon Schreibt articles: 13 | 14 | - [Homeworld 2 – Backgrounds](http://simonschreibt.de/gat/homeworld-2-backgrounds/) 15 | - [Homeworld 2 – Backgrounds Tech](http://simonschreibt.de/gat/homeworld-2-backgrounds-tech/) 16 | 17 | ## Dependencies 18 | 19 | The following Python 3.x dependencies are needed to run the program: 20 | 21 | ``` 22 | pip3 install --user pyopengl numpy glfw 23 | ``` 24 | 25 | ## Usage 26 | 27 | Launch the program from the command line with the name of the background to display, 28 | 29 | ```bash 30 | ./visualize.py background/m01.hod 31 | ``` 32 | 33 | Drag with the left mouse button pressed to rotate the view. 34 | - `w` to toggle wireframe mode. 35 | - `s` to toggle slow mode. 36 | - `esc` to quit. 37 | 38 | ## Extracting the data files 39 | 40 | The data files in `background/*` are part of the original game and cannot be distributed with 41 | this program. 42 | 43 | Extracting the .hod files can be done in the following way: 44 | 45 | - Install homeworld 2 from the CD 46 | 47 | - Apply patch to 1.1 (not sure this is needed) 48 | 49 | - Extract background/ from homeworld2.big 50 | The big file format is described in `doc/big_file_format.html` in this repository. 51 | I used the `UnfBIG` tool for this (can be found [here](http://www.homeworldaccess.net/downloads.php?cat_id=8&download_id=53) ) 52 | You just need to extract the files, no deobfuscation of the lua files is needed. 53 | 54 | - Find `.hod` files that have associated `_light.hod`. The `_light.hod` variants contain the position of the lights and 55 | are not needed at this point. 56 | 57 | These are the following: 58 | 59 | background/black.hod 60 | background/m01.hod 61 | background/m02.hod 62 | background/m03.hod 63 | background/m04.hod 64 | background/m05.hod 65 | background/m06.hod 66 | background/m07.hod 67 | background/m08.hod 68 | background/m09.hod 69 | background/m10.hod 70 | background/m11.hod 71 | background/m12.hod 72 | background/m13.hod 73 | background/m14.hod 74 | background/m15.hod 75 | background/white.hod 76 | 77 | The hod files in other directories space ship models and such. These can not currently be viewed with this 78 | tool as the vertex format is different. 79 | 80 | -------------------------------------------------------------------------------- /parse_bg.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Wladimir J. van der Laan 2 | # Distributed under the MIT/X11 software license, see the accompanying 3 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 | '''Parse homeworld 2 background''' 5 | from __future__ import division, print_function 6 | import struct 7 | 8 | NESTED={b'BGMS'} 9 | 10 | PRIM_TRIANGLES = 514 11 | PRIM_TRIANGLE_STRIP = 518 12 | 13 | def parse_BMSH(data): 14 | ''' 15 | "NRML" 16 | , {ulong} 17 | "BMSH" 18 | {ulong} 20 | {ulong} 21 | {ulong} 22 | {ulong} 23 | {ulong} ??? 24 | {ulong} 25 | 26 | , {ulong} 27 | 28 | ---- vtxd ---- 29 | {float, ie single} 30 | {float, ie single} 31 | {float, ie single} 32 | {float, ie single} 33 | {float, ie single} 34 | ---- end ---- 35 | ---- facd ---- 36 | , {ulong} 37 | , {ulong} 39 | , ushort (Important!!! This starts from zero! wOBJ starts from 1!) 40 | (Strips are a bit... different that lists, Stay Sharp) 41 | ---- end ---- 42 | ''' 43 | # From: http://forums.relicnews.com/showthread.php?99226-HOD-File-Format-%28Regardless-of-type%29 44 | version, = struct.unpack('>I', data[0:4]) 45 | #print('---------------------------------') 46 | #print('Version', version) 47 | lod,num,mat,vertsize,numverts = struct.unpack('= 3) # at least one full triangle 68 | fdata = data[ofs:ofs+2*listcount] 69 | ofs += 2*listcount 70 | facelists.append((listtype, listcount, fdata)) 71 | #print('---------------------------------') 72 | return (numverts, vertsize, vertdata, facelists) 73 | 74 | class BackgroundParser(object): 75 | def __init__(self): 76 | self.bmshes = [] 77 | 78 | def parse_block(self, data, nesting=0): 79 | ofs = 0 80 | while ofs < len(data): 81 | outer_blkid = data[ofs:ofs+4] 82 | ofs += 4 83 | 84 | size, = struct.unpack('>I', data[ofs:ofs+4]) 85 | ofs += 4 86 | 87 | blkid = data[ofs:ofs+4] # form has nested blkid 88 | ofs += 4 89 | size -= 4 90 | 91 | inner = data[ofs:ofs+size] 92 | if blkid in NESTED: 93 | self.parse_block(inner, nesting+1) 94 | if blkid == b'BMSH': 95 | bmsh = parse_BMSH(inner) 96 | self.bmshes.append(bmsh) 97 | ofs += size 98 | 99 | def parse_bg(filename): 100 | parser = BackgroundParser() 101 | f = open(filename, 'rb') 102 | parser.parse_block(f.read()) 103 | f.close() 104 | return parser.bmshes 105 | 106 | -------------------------------------------------------------------------------- /visualize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Copyright (c) 2014 Wladimir J. van der Laan 3 | # Distributed under the MIT/X11 software license, see the accompanying 4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 | ''' 6 | Show Homeworld 2 backgrounds using OpenGL-based visualization. 7 | ''' 8 | from OpenGL.extensions import alternate 9 | from OpenGL.GL import * 10 | from OpenGL.GL import shaders 11 | from OpenGL.GL.NV.primitive_restart import * 12 | import ctypes 13 | import glfw 14 | import math 15 | import numpy 16 | import os 17 | import random 18 | import time 19 | 20 | from math3d import perspective_matrix 21 | from glfw_platform import GLFWPlatform 22 | from parse_bg import parse_bg, PRIM_TRIANGLE_STRIP, PRIM_TRIANGLES 23 | from transformations import Arcball, quaternion_slerp, random_quaternion, quaternion_multiply, quaternion_about_axis 24 | 25 | window = 0 26 | w_width, w_height = 500, 400 27 | f_width, f_height = 500, 400 28 | wireframe_mode = False 29 | quit_flag = False 30 | slow_flag = False 31 | rotation_speed = 1.0 32 | arcball = Arcball() 33 | arcball.active = False 34 | animate = None # autospin 35 | fovy = 45 # field of vision in y - I don't know what original homeworld uses 36 | cur_time = nextframe_time = None 37 | 38 | # Options for primitive restart 39 | PRIMITIVE_RESTART_NONE = 0 40 | PRIMITIVE_RESTART_CORE = 1 41 | PRIMITIVE_RESTART_NV = 2 42 | 43 | # Primitive restart type supported (filled in in probe_extensions) 44 | primitive_restart_mode = PRIMITIVE_RESTART_NONE 45 | 46 | gl_types = { 47 | PRIM_TRIANGLES: GL_TRIANGLES, 48 | PRIM_TRIANGLE_STRIP: GL_TRIANGLE_STRIP 49 | } 50 | PRIMITIVE_RESTART_INDEX = 65535 51 | 52 | # extension alternates for GL <2.0 53 | from OpenGL.GL.ARB.vertex_shader import * 54 | from OpenGL.GL.ARB.vertex_buffer_object import * 55 | from OpenGL.GL.ARB.vertex_program import * 56 | glGetAttribLocation = alternate('glGetAttribLocation', glGetAttribLocation, glGetAttribLocationARB) 57 | glEnableVertexAttribArray = alternate('glEnableVertexAttribArray', glEnableVertexAttribArray, glEnableVertexAttribArrayARB) 58 | glDisableVertexAttribArray = alternate('glDisableVertexAttribArray', glDisableVertexAttribArray, glDisableVertexAttribArrayARB) 59 | glVertexAttribPointer = alternate('glVertexAttribPointer', glVertexAttribPointer, glVertexAttribPointerARB) 60 | glGenBuffers = alternate('glGenBuffers', glGenBuffers, glGenBuffersARB) 61 | glBindBuffer = alternate('glBindBuffer', glBindBuffer, glBindBufferARB) 62 | glBufferData = alternate('glBufferData', glBufferData, glBufferDataARB) 63 | 64 | # GLFW window hints for wayland 65 | # This needs https://github.com/glfw/glfw/pull/2061 66 | GLFW_WAYLAND_SHELL_LAYER = 0x00026001 67 | ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0 68 | 69 | def force_rerender(): 70 | ''' 71 | Force a re-render on next possible opportunity. 72 | ''' 73 | global cur_time, nextframe_time 74 | nextframe_time = cur_time 75 | 76 | def error_callback(code, message): 77 | print(f'{code} {message.decode()}') 78 | 79 | def window_size_callback(window, w, h): 80 | global w_width, w_height 81 | w_width = w 82 | w_height = h 83 | arcball.place([w/2, h/2], h/2) 84 | 85 | def framebuffer_size_callback(window, w, h): 86 | global f_width, f_height 87 | f_width = w 88 | f_height = h 89 | force_rerender() 90 | 91 | def draw(): 92 | glViewport(0, 0, f_width, f_height) 93 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 94 | 95 | # set up matrices 96 | glMatrixMode(GL_PROJECTION) 97 | glLoadMatrixf(perspective_matrix(fovy, f_width/f_height, 1.0, 100.0)) 98 | glMatrixMode(GL_MODELVIEW) 99 | glLoadMatrixf(arcball.matrix().T) 100 | 101 | # rendering time 102 | if wireframe_mode: 103 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 104 | else: 105 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 106 | 107 | if primitive_restart_mode == PRIMITIVE_RESTART_CORE: 108 | glEnable(GL_PRIMITIVE_RESTART) 109 | glPrimitiveRestartIndex(PRIMITIVE_RESTART_INDEX) 110 | elif primitive_restart_mode == PRIMITIVE_RESTART_NV: 111 | glEnableClientState(GL_PRIMITIVE_RESTART_NV) 112 | glPrimitiveRestartIndexNV(PRIMITIVE_RESTART_INDEX) 113 | 114 | shaders.glUseProgram(background_shader) 115 | glBindBuffer(GL_ARRAY_BUFFER, vbo) 116 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) 117 | glEnableVertexAttribArray(vertex_loc) 118 | glEnableVertexAttribArray(color_loc) 119 | prims = 0 120 | for numverts,vertsize,vertdata_offset,facelists in nbgdata: 121 | glVertexAttribPointer(vertex_loc, 4, GL_FLOAT, False, vertsize, ctypes.c_void_p(vertdata_offset)) 122 | glVertexAttribPointer(color_loc, 4, GL_BYTE, True, vertsize, ctypes.c_void_p(vertdata_offset+16)) 123 | for typ, count, facedata_offset in facelists: 124 | glDrawElements(gl_types[typ], count, GL_UNSIGNED_SHORT, ctypes.c_void_p(facedata_offset)) 125 | glDisableVertexAttribArray(vertex_loc) 126 | glDisableVertexAttribArray(color_loc) 127 | glBindBuffer(GL_ARRAY_BUFFER, 0) 128 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) 129 | shaders.glUseProgram(0) 130 | if primitive_restart_mode == PRIMITIVE_RESTART_CORE: 131 | glDisable(GL_PRIMITIVE_RESTART) 132 | elif primitive_restart_mode == PRIMITIVE_RESTART_NV: 133 | glDisableClientState(GL_PRIMITIVE_RESTART_NV) 134 | 135 | def advance_time(deltatime): 136 | global animate 137 | if animate is not None: 138 | # Continue in auto-spin if arcball not active 139 | animate[2] += deltatime * 20.0 140 | arcball._qnow = quaternion_slerp(animate[0], animate[1], animate[2], False) 141 | 142 | def key_callback(window, key, scancode, action, mods): 143 | ''' 144 | Keyboard: w for wireframe mode 145 | ''' 146 | global wireframe_mode, slow_flag, quit_flag 147 | if action == glfw.PRESS and mods == 0: 148 | if key == glfw.KEY_W: 149 | wireframe_mode = not wireframe_mode 150 | if key == glfw.KEY_S: 151 | slow_flag = not slow_flag 152 | elif key == glfw.KEY_ESCAPE: 153 | quit_flag = True 154 | 155 | def mouse_button_callback(window, button, action, mods): 156 | global animate 157 | (x, y) = glfw.get_cursor_pos(window) 158 | if button == 0 and mods == 0: 159 | arcball.active = (action == glfw.PRESS) 160 | if arcball.active: 161 | arcball.down([x,y]) 162 | animate = None 163 | elif numpy.allclose(arcball._qpre, arcball._qnow): # effectively no animation, save CPU cycles 164 | animate = None 165 | else: 166 | animate = [arcball._qpre, arcball._qnow, 1.0] 167 | force_rerender() 168 | 169 | def cursor_pos_callback(window, x, y): 170 | if arcball.active: 171 | arcball.drag([x,y]) 172 | force_rerender() 173 | 174 | def create_shaders(): 175 | global background_shader, vertex_loc, color_loc 176 | 177 | VERTEX_SHADER = shaders.compileShader(b""" 178 | #version 120 179 | attribute vec4 inVertex; 180 | attribute vec4 inColor; 181 | void main() 182 | { 183 | gl_Position = gl_ModelViewProjectionMatrix * inVertex; 184 | gl_FrontColor = inColor.abgr; 185 | } 186 | """, GL_VERTEX_SHADER) 187 | 188 | FRAGMENT_SHADER = shaders.compileShader(b""" 189 | #version 120 190 | void main() 191 | { 192 | gl_FragColor = gl_Color; 193 | }""", GL_FRAGMENT_SHADER) 194 | background_shader = shaders.compileProgram(VERTEX_SHADER,FRAGMENT_SHADER) 195 | vertex_loc = glGetAttribLocation(background_shader, b"inVertex") 196 | color_loc = glGetAttribLocation(background_shader, b"inColor") 197 | 198 | def create_vbos(bgdata): 199 | global ibo,vbo,nbgdata 200 | # Build vertex and index buffers and new bgdata structure 201 | # that has pointers into the vertex and index buffers instead of 202 | # data 203 | allvertdata = [] 204 | allfacedata = [] 205 | vertdata_ptr = 0 206 | facedata_ptr = 0 207 | nbgdata = [] 208 | for numverts,vertsize,vertdata,facelists in bgdata: 209 | vertdata_offset = vertdata_ptr 210 | allvertdata.append(vertdata) 211 | vertdata_ptr += len(vertdata) 212 | nfacelists = [] 213 | for typ, count, facedata in facelists: 214 | facedata_offset = facedata_ptr 215 | allfacedata.append(facedata) 216 | facedata_ptr += len(facedata) 217 | nfacelists.append((typ, count, facedata_offset)) 218 | nbgdata.append((numverts, vertsize, vertdata_offset, nfacelists)) 219 | 220 | allvertdata = b''.join(allvertdata) 221 | allfacedata = b''.join(allfacedata) 222 | 223 | vbo = glGenBuffers(1) 224 | glBindBuffer(GL_ARRAY_BUFFER, vbo) 225 | glBufferData(GL_ARRAY_BUFFER, len(allvertdata), allvertdata, GL_STATIC_DRAW) 226 | glBindBuffer(GL_ARRAY_BUFFER, 0) 227 | 228 | ibo = glGenBuffers(1) 229 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) 230 | glBufferData(GL_ELEMENT_ARRAY_BUFFER, len(allfacedata), allfacedata, GL_STATIC_DRAW) 231 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) 232 | 233 | def concatenate_primitives(bgdata): 234 | import struct 235 | PRIMITIVE_RESTART = struct.pack('= nextframe_time: 362 | draw() 363 | glfw.swap_buffers(window) 364 | 365 | if animate is not None: 366 | if slow_flag: # if slow, only render once per second 367 | nextframe_time = cur_time + 1.0 368 | else: 369 | nextframe_time = cur_time 370 | else: 371 | nextframe_time = None 372 | 373 | last_time = cur_time 374 | 375 | if nextframe_time is not None: 376 | if nextframe_time > cur_time: 377 | glfw.wait_events_timeout(nextframe_time - cur_time) 378 | else: 379 | glfw.poll_events() 380 | else: 381 | glfw.wait_events() 382 | 383 | 384 | if __name__ == '__main__': 385 | main() 386 | -------------------------------------------------------------------------------- /transformations.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # -*- coding: utf-8 -*- 4 | # transformations.py 5 | 6 | # Copyright (c) 2006-2014, Christoph Gohlke 7 | # Copyright (c) 2006-2014, The Regents of the University of California 8 | # Produced at the Laboratory for Fluorescence Dynamics 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without 12 | # modification, are permitted provided that the following conditions are met: 13 | # 14 | # * Redistributions of source code must retain the above copyright 15 | # notice, this list of conditions and the following disclaimer. 16 | # * Redistributions in binary form must reproduce the above copyright 17 | # notice, this list of conditions and the following disclaimer in the 18 | # documentation and/or other materials provided with the distribution. 19 | # * Neither the name of the copyright holders nor the names of any 20 | # contributors may be used to endorse or promote products derived 21 | # from this software without specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 26 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 27 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 28 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 29 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 30 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 31 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 32 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | # POSSIBILITY OF SUCH DAMAGE. 34 | 35 | """Homogeneous Transformation Matrices and Quaternions. 36 | 37 | A library for calculating 4x4 matrices for translating, rotating, reflecting, 38 | scaling, shearing, projecting, orthogonalizing, and superimposing arrays of 39 | 3D homogeneous coordinates as well as for converting between rotation matrices, 40 | Euler angles, and quaternions. Also includes an Arcball control object and 41 | functions to decompose transformation matrices. 42 | 43 | :Author: 44 | `Christoph Gohlke `_ 45 | 46 | :Organization: 47 | Laboratory for Fluorescence Dynamics, University of California, Irvine 48 | 49 | :Version: 2013.06.29 50 | 51 | Requirements 52 | ------------ 53 | * `CPython 2.7 or 3.3 `_ 54 | * `Numpy 1.7 `_ 55 | * `Transformations.c 2013.01.18 `_ 56 | (recommended for speedup of some functions) 57 | 58 | Notes 59 | ----- 60 | The API is not stable yet and is expected to change between revisions. 61 | 62 | This Python code is not optimized for speed. Refer to the transformations.c 63 | module for a faster implementation of some functions. 64 | 65 | Documentation in HTML format can be generated with epydoc. 66 | 67 | Matrices (M) can be inverted using numpy.linalg.inv(M), be concatenated using 68 | numpy.dot(M0, M1), or transform homogeneous coordinate arrays (v) using 69 | numpy.dot(M, v) for shape (4, \*) column vectors, respectively 70 | numpy.dot(v, M.T) for shape (\*, 4) row vectors ("array of points"). 71 | 72 | This module follows the "column vectors on the right" and "row major storage" 73 | (C contiguous) conventions. The translation components are in the right column 74 | of the transformation matrix, i.e. M[:3, 3]. 75 | The transpose of the transformation matrices may have to be used to interface 76 | with other graphics systems, e.g. with OpenGL's glMultMatrixd(). See also [16]. 77 | 78 | Calculations are carried out with numpy.float64 precision. 79 | 80 | Vector, point, quaternion, and matrix function arguments are expected to be 81 | "array like", i.e. tuple, list, or numpy arrays. 82 | 83 | Return types are numpy arrays unless specified otherwise. 84 | 85 | Angles are in radians unless specified otherwise. 86 | 87 | Quaternions w+ix+jy+kz are represented as [w, x, y, z]. 88 | 89 | A triple of Euler angles can be applied/interpreted in 24 ways, which can 90 | be specified using a 4 character string or encoded 4-tuple: 91 | 92 | *Axes 4-string*: e.g. 'sxyz' or 'ryxy' 93 | 94 | - first character : rotations are applied to 's'tatic or 'r'otating frame 95 | - remaining characters : successive rotation axis 'x', 'y', or 'z' 96 | 97 | *Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1) 98 | 99 | - inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix. 100 | - parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed 101 | by 'z', or 'z' is followed by 'x'. Otherwise odd (1). 102 | - repetition : first and last axis are same (1) or different (0). 103 | - frame : rotations are applied to static (0) or rotating (1) frame. 104 | 105 | References 106 | ---------- 107 | (1) Matrices and transformations. Ronald Goldman. 108 | In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990. 109 | (2) More matrices and transformations: shear and pseudo-perspective. 110 | Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991. 111 | (3) Decomposing a matrix into simple transformations. Spencer Thomas. 112 | In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991. 113 | (4) Recovering the data from the transformation matrix. Ronald Goldman. 114 | In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991. 115 | (5) Euler angle conversion. Ken Shoemake. 116 | In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994. 117 | (6) Arcball rotation control. Ken Shoemake. 118 | In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994. 119 | (7) Representing attitude: Euler angles, unit quaternions, and rotation 120 | vectors. James Diebel. 2006. 121 | (8) A discussion of the solution for the best rotation to relate two sets 122 | of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828. 123 | (9) Closed-form solution of absolute orientation using unit quaternions. 124 | BKP Horn. J Opt Soc Am A. 1987. 4(4):629-642. 125 | (10) Quaternions. Ken Shoemake. 126 | http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf 127 | (11) From quaternion to matrix and back. JMP van Waveren. 2005. 128 | http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm 129 | (12) Uniform random rotations. Ken Shoemake. 130 | In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992. 131 | (13) Quaternion in molecular modeling. CFF Karney. 132 | J Mol Graph Mod, 25(5):595-604 133 | (14) New method for extracting the quaternion from a rotation matrix. 134 | Itzhack Y Bar-Itzhack, J Guid Contr Dynam. 2000. 23(6): 1085-1087. 135 | (15) Multiple View Geometry in Computer Vision. Hartley and Zissermann. 136 | Cambridge University Press; 2nd Ed. 2004. Chapter 4, Algorithm 4.7, p 130. 137 | (16) Column Vectors vs. Row Vectors. 138 | http://steve.hollasch.net/cgindex/math/matrix/column-vec.html 139 | 140 | Examples 141 | -------- 142 | >>> alpha, beta, gamma = 0.123, -1.234, 2.345 143 | >>> origin, xaxis, yaxis, zaxis = [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1] 144 | >>> I = identity_matrix() 145 | >>> Rx = rotation_matrix(alpha, xaxis) 146 | >>> Ry = rotation_matrix(beta, yaxis) 147 | >>> Rz = rotation_matrix(gamma, zaxis) 148 | >>> R = concatenate_matrices(Rx, Ry, Rz) 149 | >>> euler = euler_from_matrix(R, 'rxyz') 150 | >>> numpy.allclose([alpha, beta, gamma], euler) 151 | True 152 | >>> Re = euler_matrix(alpha, beta, gamma, 'rxyz') 153 | >>> is_same_transform(R, Re) 154 | True 155 | >>> al, be, ga = euler_from_matrix(Re, 'rxyz') 156 | >>> is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz')) 157 | True 158 | >>> qx = quaternion_about_axis(alpha, xaxis) 159 | >>> qy = quaternion_about_axis(beta, yaxis) 160 | >>> qz = quaternion_about_axis(gamma, zaxis) 161 | >>> q = quaternion_multiply(qx, qy) 162 | >>> q = quaternion_multiply(q, qz) 163 | >>> Rq = quaternion_matrix(q) 164 | >>> is_same_transform(R, Rq) 165 | True 166 | >>> S = scale_matrix(1.23, origin) 167 | >>> T = translation_matrix([1, 2, 3]) 168 | >>> Z = shear_matrix(beta, xaxis, origin, zaxis) 169 | >>> R = random_rotation_matrix(numpy.random.rand(3)) 170 | >>> M = concatenate_matrices(T, R, Z, S) 171 | >>> scale, shear, angles, trans, persp = decompose_matrix(M) 172 | >>> numpy.allclose(scale, 1.23) 173 | True 174 | >>> numpy.allclose(trans, [1, 2, 3]) 175 | True 176 | >>> numpy.allclose(shear, [0, math.tan(beta), 0]) 177 | True 178 | >>> is_same_transform(R, euler_matrix(axes='sxyz', *angles)) 179 | True 180 | >>> M1 = compose_matrix(scale, shear, angles, trans, persp) 181 | >>> is_same_transform(M, M1) 182 | True 183 | >>> v0, v1 = random_vector(3), random_vector(3) 184 | >>> M = rotation_matrix(angle_between_vectors(v0, v1), vector_product(v0, v1)) 185 | >>> v2 = numpy.dot(v0, M[:3,:3].T) 186 | >>> numpy.allclose(unit_vector(v1), unit_vector(v2)) 187 | True 188 | 189 | """ 190 | 191 | from __future__ import division, print_function 192 | 193 | import math 194 | 195 | import numpy 196 | 197 | __version__ = '2013.06.29' 198 | __docformat__ = 'restructuredtext en' 199 | __all__ = [] 200 | 201 | 202 | def identity_matrix(): 203 | """Return 4x4 identity/unit matrix. 204 | 205 | >>> I = identity_matrix() 206 | >>> numpy.allclose(I, numpy.dot(I, I)) 207 | True 208 | >>> numpy.sum(I), numpy.trace(I) 209 | (4.0, 4.0) 210 | >>> numpy.allclose(I, numpy.identity(4)) 211 | True 212 | 213 | """ 214 | return numpy.identity(4) 215 | 216 | 217 | def translation_matrix(direction): 218 | """Return matrix to translate by direction vector. 219 | 220 | >>> v = numpy.random.random(3) - 0.5 221 | >>> numpy.allclose(v, translation_matrix(v)[:3, 3]) 222 | True 223 | 224 | """ 225 | M = numpy.identity(4) 226 | M[:3, 3] = direction[:3] 227 | return M 228 | 229 | 230 | def translation_from_matrix(matrix): 231 | """Return translation vector from translation matrix. 232 | 233 | >>> v0 = numpy.random.random(3) - 0.5 234 | >>> v1 = translation_from_matrix(translation_matrix(v0)) 235 | >>> numpy.allclose(v0, v1) 236 | True 237 | 238 | """ 239 | return numpy.array(matrix, copy=False)[:3, 3].copy() 240 | 241 | 242 | def reflection_matrix(point, normal): 243 | """Return matrix to mirror at plane defined by point and normal vector. 244 | 245 | >>> v0 = numpy.random.random(4) - 0.5 246 | >>> v0[3] = 1. 247 | >>> v1 = numpy.random.random(3) - 0.5 248 | >>> R = reflection_matrix(v0, v1) 249 | >>> numpy.allclose(2, numpy.trace(R)) 250 | True 251 | >>> numpy.allclose(v0, numpy.dot(R, v0)) 252 | True 253 | >>> v2 = v0.copy() 254 | >>> v2[:3] += v1 255 | >>> v3 = v0.copy() 256 | >>> v2[:3] -= v1 257 | >>> numpy.allclose(v2, numpy.dot(R, v3)) 258 | True 259 | 260 | """ 261 | normal = unit_vector(normal[:3]) 262 | M = numpy.identity(4) 263 | M[:3, :3] -= 2.0 * numpy.outer(normal, normal) 264 | M[:3, 3] = (2.0 * numpy.dot(point[:3], normal)) * normal 265 | return M 266 | 267 | 268 | def reflection_from_matrix(matrix): 269 | """Return mirror plane point and normal vector from reflection matrix. 270 | 271 | >>> v0 = numpy.random.random(3) - 0.5 272 | >>> v1 = numpy.random.random(3) - 0.5 273 | >>> M0 = reflection_matrix(v0, v1) 274 | >>> point, normal = reflection_from_matrix(M0) 275 | >>> M1 = reflection_matrix(point, normal) 276 | >>> is_same_transform(M0, M1) 277 | True 278 | 279 | """ 280 | M = numpy.array(matrix, dtype=numpy.float64, copy=False) 281 | # normal: unit eigenvector corresponding to eigenvalue -1 282 | w, V = numpy.linalg.eig(M[:3, :3]) 283 | i = numpy.where(abs(numpy.real(w) + 1.0) < 1e-8)[0] 284 | if not len(i): 285 | raise ValueError("no unit eigenvector corresponding to eigenvalue -1") 286 | normal = numpy.real(V[:, i[0]]).squeeze() 287 | # point: any unit eigenvector corresponding to eigenvalue 1 288 | w, V = numpy.linalg.eig(M) 289 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] 290 | if not len(i): 291 | raise ValueError("no unit eigenvector corresponding to eigenvalue 1") 292 | point = numpy.real(V[:, i[-1]]).squeeze() 293 | point /= point[3] 294 | return point, normal 295 | 296 | 297 | def rotation_matrix(angle, direction, point=None): 298 | """Return matrix to rotate about axis defined by point and direction. 299 | 300 | >>> R = rotation_matrix(math.pi/2, [0, 0, 1], [1, 0, 0]) 301 | >>> numpy.allclose(numpy.dot(R, [0, 0, 0, 1]), [1, -1, 0, 1]) 302 | True 303 | >>> angle = (random.random() - 0.5) * (2*math.pi) 304 | >>> direc = numpy.random.random(3) - 0.5 305 | >>> point = numpy.random.random(3) - 0.5 306 | >>> R0 = rotation_matrix(angle, direc, point) 307 | >>> R1 = rotation_matrix(angle-2*math.pi, direc, point) 308 | >>> is_same_transform(R0, R1) 309 | True 310 | >>> R0 = rotation_matrix(angle, direc, point) 311 | >>> R1 = rotation_matrix(-angle, -direc, point) 312 | >>> is_same_transform(R0, R1) 313 | True 314 | >>> I = numpy.identity(4, numpy.float64) 315 | >>> numpy.allclose(I, rotation_matrix(math.pi*2, direc)) 316 | True 317 | >>> numpy.allclose(2, numpy.trace(rotation_matrix(math.pi/2, 318 | ... direc, point))) 319 | True 320 | 321 | """ 322 | sina = math.sin(angle) 323 | cosa = math.cos(angle) 324 | direction = unit_vector(direction[:3]) 325 | # rotation matrix around unit vector 326 | R = numpy.diag([cosa, cosa, cosa]) 327 | R += numpy.outer(direction, direction) * (1.0 - cosa) 328 | direction *= sina 329 | R += numpy.array([[ 0.0, -direction[2], direction[1]], 330 | [ direction[2], 0.0, -direction[0]], 331 | [-direction[1], direction[0], 0.0]]) 332 | M = numpy.identity(4) 333 | M[:3, :3] = R 334 | if point is not None: 335 | # rotation not around origin 336 | point = numpy.array(point[:3], dtype=numpy.float64, copy=False) 337 | M[:3, 3] = point - numpy.dot(R, point) 338 | return M 339 | 340 | 341 | def rotation_from_matrix(matrix): 342 | """Return rotation angle and axis from rotation matrix. 343 | 344 | >>> angle = (random.random() - 0.5) * (2*math.pi) 345 | >>> direc = numpy.random.random(3) - 0.5 346 | >>> point = numpy.random.random(3) - 0.5 347 | >>> R0 = rotation_matrix(angle, direc, point) 348 | >>> angle, direc, point = rotation_from_matrix(R0) 349 | >>> R1 = rotation_matrix(angle, direc, point) 350 | >>> is_same_transform(R0, R1) 351 | True 352 | 353 | """ 354 | R = numpy.array(matrix, dtype=numpy.float64, copy=False) 355 | R33 = R[:3, :3] 356 | # direction: unit eigenvector of R33 corresponding to eigenvalue of 1 357 | w, W = numpy.linalg.eig(R33.T) 358 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] 359 | if not len(i): 360 | raise ValueError("no unit eigenvector corresponding to eigenvalue 1") 361 | direction = numpy.real(W[:, i[-1]]).squeeze() 362 | # point: unit eigenvector of R33 corresponding to eigenvalue of 1 363 | w, Q = numpy.linalg.eig(R) 364 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] 365 | if not len(i): 366 | raise ValueError("no unit eigenvector corresponding to eigenvalue 1") 367 | point = numpy.real(Q[:, i[-1]]).squeeze() 368 | point /= point[3] 369 | # rotation angle depending on direction 370 | cosa = (numpy.trace(R33) - 1.0) / 2.0 371 | if abs(direction[2]) > 1e-8: 372 | sina = (R[1, 0] + (cosa-1.0)*direction[0]*direction[1]) / direction[2] 373 | elif abs(direction[1]) > 1e-8: 374 | sina = (R[0, 2] + (cosa-1.0)*direction[0]*direction[2]) / direction[1] 375 | else: 376 | sina = (R[2, 1] + (cosa-1.0)*direction[1]*direction[2]) / direction[0] 377 | angle = math.atan2(sina, cosa) 378 | return angle, direction, point 379 | 380 | 381 | def scale_matrix(factor, origin=None, direction=None): 382 | """Return matrix to scale by factor around origin in direction. 383 | 384 | Use factor -1 for point symmetry. 385 | 386 | >>> v = (numpy.random.rand(4, 5) - 0.5) * 20 387 | >>> v[3] = 1 388 | >>> S = scale_matrix(-1.234) 389 | >>> numpy.allclose(numpy.dot(S, v)[:3], -1.234*v[:3]) 390 | True 391 | >>> factor = random.random() * 10 - 5 392 | >>> origin = numpy.random.random(3) - 0.5 393 | >>> direct = numpy.random.random(3) - 0.5 394 | >>> S = scale_matrix(factor, origin) 395 | >>> S = scale_matrix(factor, origin, direct) 396 | 397 | """ 398 | if direction is None: 399 | # uniform scaling 400 | M = numpy.diag([factor, factor, factor, 1.0]) 401 | if origin is not None: 402 | M[:3, 3] = origin[:3] 403 | M[:3, 3] *= 1.0 - factor 404 | else: 405 | # nonuniform scaling 406 | direction = unit_vector(direction[:3]) 407 | factor = 1.0 - factor 408 | M = numpy.identity(4) 409 | M[:3, :3] -= factor * numpy.outer(direction, direction) 410 | if origin is not None: 411 | M[:3, 3] = (factor * numpy.dot(origin[:3], direction)) * direction 412 | return M 413 | 414 | 415 | def scale_from_matrix(matrix): 416 | """Return scaling factor, origin and direction from scaling matrix. 417 | 418 | >>> factor = random.random() * 10 - 5 419 | >>> origin = numpy.random.random(3) - 0.5 420 | >>> direct = numpy.random.random(3) - 0.5 421 | >>> S0 = scale_matrix(factor, origin) 422 | >>> factor, origin, direction = scale_from_matrix(S0) 423 | >>> S1 = scale_matrix(factor, origin, direction) 424 | >>> is_same_transform(S0, S1) 425 | True 426 | >>> S0 = scale_matrix(factor, origin, direct) 427 | >>> factor, origin, direction = scale_from_matrix(S0) 428 | >>> S1 = scale_matrix(factor, origin, direction) 429 | >>> is_same_transform(S0, S1) 430 | True 431 | 432 | """ 433 | M = numpy.array(matrix, dtype=numpy.float64, copy=False) 434 | M33 = M[:3, :3] 435 | factor = numpy.trace(M33) - 2.0 436 | try: 437 | # direction: unit eigenvector corresponding to eigenvalue factor 438 | w, V = numpy.linalg.eig(M33) 439 | i = numpy.where(abs(numpy.real(w) - factor) < 1e-8)[0][0] 440 | direction = numpy.real(V[:, i]).squeeze() 441 | direction /= vector_norm(direction) 442 | except IndexError: 443 | # uniform scaling 444 | factor = (factor + 2.0) / 3.0 445 | direction = None 446 | # origin: any eigenvector corresponding to eigenvalue 1 447 | w, V = numpy.linalg.eig(M) 448 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] 449 | if not len(i): 450 | raise ValueError("no eigenvector corresponding to eigenvalue 1") 451 | origin = numpy.real(V[:, i[-1]]).squeeze() 452 | origin /= origin[3] 453 | return factor, origin, direction 454 | 455 | 456 | def projection_matrix(point, normal, direction=None, 457 | perspective=None, pseudo=False): 458 | """Return matrix to project onto plane defined by point and normal. 459 | 460 | Using either perspective point, projection direction, or none of both. 461 | 462 | If pseudo is True, perspective projections will preserve relative depth 463 | such that Perspective = dot(Orthogonal, PseudoPerspective). 464 | 465 | >>> P = projection_matrix([0, 0, 0], [1, 0, 0]) 466 | >>> numpy.allclose(P[1:, 1:], numpy.identity(4)[1:, 1:]) 467 | True 468 | >>> point = numpy.random.random(3) - 0.5 469 | >>> normal = numpy.random.random(3) - 0.5 470 | >>> direct = numpy.random.random(3) - 0.5 471 | >>> persp = numpy.random.random(3) - 0.5 472 | >>> P0 = projection_matrix(point, normal) 473 | >>> P1 = projection_matrix(point, normal, direction=direct) 474 | >>> P2 = projection_matrix(point, normal, perspective=persp) 475 | >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True) 476 | >>> is_same_transform(P2, numpy.dot(P0, P3)) 477 | True 478 | >>> P = projection_matrix([3, 0, 0], [1, 1, 0], [1, 0, 0]) 479 | >>> v0 = (numpy.random.rand(4, 5) - 0.5) * 20 480 | >>> v0[3] = 1 481 | >>> v1 = numpy.dot(P, v0) 482 | >>> numpy.allclose(v1[1], v0[1]) 483 | True 484 | >>> numpy.allclose(v1[0], 3-v1[1]) 485 | True 486 | 487 | """ 488 | M = numpy.identity(4) 489 | point = numpy.array(point[:3], dtype=numpy.float64, copy=False) 490 | normal = unit_vector(normal[:3]) 491 | if perspective is not None: 492 | # perspective projection 493 | perspective = numpy.array(perspective[:3], dtype=numpy.float64, 494 | copy=False) 495 | M[0, 0] = M[1, 1] = M[2, 2] = numpy.dot(perspective-point, normal) 496 | M[:3, :3] -= numpy.outer(perspective, normal) 497 | if pseudo: 498 | # preserve relative depth 499 | M[:3, :3] -= numpy.outer(normal, normal) 500 | M[:3, 3] = numpy.dot(point, normal) * (perspective+normal) 501 | else: 502 | M[:3, 3] = numpy.dot(point, normal) * perspective 503 | M[3, :3] = -normal 504 | M[3, 3] = numpy.dot(perspective, normal) 505 | elif direction is not None: 506 | # parallel projection 507 | direction = numpy.array(direction[:3], dtype=numpy.float64, copy=False) 508 | scale = numpy.dot(direction, normal) 509 | M[:3, :3] -= numpy.outer(direction, normal) / scale 510 | M[:3, 3] = direction * (numpy.dot(point, normal) / scale) 511 | else: 512 | # orthogonal projection 513 | M[:3, :3] -= numpy.outer(normal, normal) 514 | M[:3, 3] = numpy.dot(point, normal) * normal 515 | return M 516 | 517 | 518 | def projection_from_matrix(matrix, pseudo=False): 519 | """Return projection plane and perspective point from projection matrix. 520 | 521 | Return values are same as arguments for projection_matrix function: 522 | point, normal, direction, perspective, and pseudo. 523 | 524 | >>> point = numpy.random.random(3) - 0.5 525 | >>> normal = numpy.random.random(3) - 0.5 526 | >>> direct = numpy.random.random(3) - 0.5 527 | >>> persp = numpy.random.random(3) - 0.5 528 | >>> P0 = projection_matrix(point, normal) 529 | >>> result = projection_from_matrix(P0) 530 | >>> P1 = projection_matrix(*result) 531 | >>> is_same_transform(P0, P1) 532 | True 533 | >>> P0 = projection_matrix(point, normal, direct) 534 | >>> result = projection_from_matrix(P0) 535 | >>> P1 = projection_matrix(*result) 536 | >>> is_same_transform(P0, P1) 537 | True 538 | >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False) 539 | >>> result = projection_from_matrix(P0, pseudo=False) 540 | >>> P1 = projection_matrix(*result) 541 | >>> is_same_transform(P0, P1) 542 | True 543 | >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True) 544 | >>> result = projection_from_matrix(P0, pseudo=True) 545 | >>> P1 = projection_matrix(*result) 546 | >>> is_same_transform(P0, P1) 547 | True 548 | 549 | """ 550 | M = numpy.array(matrix, dtype=numpy.float64, copy=False) 551 | M33 = M[:3, :3] 552 | w, V = numpy.linalg.eig(M) 553 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] 554 | if not pseudo and len(i): 555 | # point: any eigenvector corresponding to eigenvalue 1 556 | point = numpy.real(V[:, i[-1]]).squeeze() 557 | point /= point[3] 558 | # direction: unit eigenvector corresponding to eigenvalue 0 559 | w, V = numpy.linalg.eig(M33) 560 | i = numpy.where(abs(numpy.real(w)) < 1e-8)[0] 561 | if not len(i): 562 | raise ValueError("no eigenvector corresponding to eigenvalue 0") 563 | direction = numpy.real(V[:, i[0]]).squeeze() 564 | direction /= vector_norm(direction) 565 | # normal: unit eigenvector of M33.T corresponding to eigenvalue 0 566 | w, V = numpy.linalg.eig(M33.T) 567 | i = numpy.where(abs(numpy.real(w)) < 1e-8)[0] 568 | if len(i): 569 | # parallel projection 570 | normal = numpy.real(V[:, i[0]]).squeeze() 571 | normal /= vector_norm(normal) 572 | return point, normal, direction, None, False 573 | else: 574 | # orthogonal projection, where normal equals direction vector 575 | return point, direction, None, None, False 576 | else: 577 | # perspective projection 578 | i = numpy.where(abs(numpy.real(w)) > 1e-8)[0] 579 | if not len(i): 580 | raise ValueError( 581 | "no eigenvector not corresponding to eigenvalue 0") 582 | point = numpy.real(V[:, i[-1]]).squeeze() 583 | point /= point[3] 584 | normal = - M[3, :3] 585 | perspective = M[:3, 3] / numpy.dot(point[:3], normal) 586 | if pseudo: 587 | perspective -= normal 588 | return point, normal, None, perspective, pseudo 589 | 590 | 591 | def clip_matrix(left, right, bottom, top, near, far, perspective=False): 592 | """Return matrix to obtain normalized device coordinates from frustum. 593 | 594 | The frustum bounds are axis-aligned along x (left, right), 595 | y (bottom, top) and z (near, far). 596 | 597 | Normalized device coordinates are in range [-1, 1] if coordinates are 598 | inside the frustum. 599 | 600 | If perspective is True the frustum is a truncated pyramid with the 601 | perspective point at origin and direction along z axis, otherwise an 602 | orthographic canonical view volume (a box). 603 | 604 | Homogeneous coordinates transformed by the perspective clip matrix 605 | need to be dehomogenized (divided by w coordinate). 606 | 607 | >>> frustum = numpy.random.rand(6) 608 | >>> frustum[1] += frustum[0] 609 | >>> frustum[3] += frustum[2] 610 | >>> frustum[5] += frustum[4] 611 | >>> M = clip_matrix(perspective=False, *frustum) 612 | >>> numpy.dot(M, [frustum[0], frustum[2], frustum[4], 1]) 613 | array([-1., -1., -1., 1.]) 614 | >>> numpy.dot(M, [frustum[1], frustum[3], frustum[5], 1]) 615 | array([ 1., 1., 1., 1.]) 616 | >>> M = clip_matrix(perspective=True, *frustum) 617 | >>> v = numpy.dot(M, [frustum[0], frustum[2], frustum[4], 1]) 618 | >>> v / v[3] 619 | array([-1., -1., -1., 1.]) 620 | >>> v = numpy.dot(M, [frustum[1], frustum[3], frustum[4], 1]) 621 | >>> v / v[3] 622 | array([ 1., 1., -1., 1.]) 623 | 624 | """ 625 | if left >= right or bottom >= top or near >= far: 626 | raise ValueError("invalid frustum") 627 | if perspective: 628 | if near <= _EPS: 629 | raise ValueError("invalid frustum: near <= 0") 630 | t = 2.0 * near 631 | M = [[t/(left-right), 0.0, (right+left)/(right-left), 0.0], 632 | [0.0, t/(bottom-top), (top+bottom)/(top-bottom), 0.0], 633 | [0.0, 0.0, (far+near)/(near-far), t*far/(far-near)], 634 | [0.0, 0.0, -1.0, 0.0]] 635 | else: 636 | M = [[2.0/(right-left), 0.0, 0.0, (right+left)/(left-right)], 637 | [0.0, 2.0/(top-bottom), 0.0, (top+bottom)/(bottom-top)], 638 | [0.0, 0.0, 2.0/(far-near), (far+near)/(near-far)], 639 | [0.0, 0.0, 0.0, 1.0]] 640 | return numpy.array(M) 641 | 642 | 643 | def shear_matrix(angle, direction, point, normal): 644 | """Return matrix to shear by angle along direction vector on shear plane. 645 | 646 | The shear plane is defined by a point and normal vector. The direction 647 | vector must be orthogonal to the plane's normal vector. 648 | 649 | A point P is transformed by the shear matrix into P" such that 650 | the vector P-P" is parallel to the direction vector and its extent is 651 | given by the angle of P-P'-P", where P' is the orthogonal projection 652 | of P onto the shear plane. 653 | 654 | >>> angle = (random.random() - 0.5) * 4*math.pi 655 | >>> direct = numpy.random.random(3) - 0.5 656 | >>> point = numpy.random.random(3) - 0.5 657 | >>> normal = numpy.cross(direct, numpy.random.random(3)) 658 | >>> S = shear_matrix(angle, direct, point, normal) 659 | >>> numpy.allclose(1, numpy.linalg.det(S)) 660 | True 661 | 662 | """ 663 | normal = unit_vector(normal[:3]) 664 | direction = unit_vector(direction[:3]) 665 | if abs(numpy.dot(normal, direction)) > 1e-6: 666 | raise ValueError("direction and normal vectors are not orthogonal") 667 | angle = math.tan(angle) 668 | M = numpy.identity(4) 669 | M[:3, :3] += angle * numpy.outer(direction, normal) 670 | M[:3, 3] = -angle * numpy.dot(point[:3], normal) * direction 671 | return M 672 | 673 | 674 | def shear_from_matrix(matrix): 675 | """Return shear angle, direction and plane from shear matrix. 676 | 677 | >>> angle = (random.random() - 0.5) * 4*math.pi 678 | >>> direct = numpy.random.random(3) - 0.5 679 | >>> point = numpy.random.random(3) - 0.5 680 | >>> normal = numpy.cross(direct, numpy.random.random(3)) 681 | >>> S0 = shear_matrix(angle, direct, point, normal) 682 | >>> angle, direct, point, normal = shear_from_matrix(S0) 683 | >>> S1 = shear_matrix(angle, direct, point, normal) 684 | >>> is_same_transform(S0, S1) 685 | True 686 | 687 | """ 688 | M = numpy.array(matrix, dtype=numpy.float64, copy=False) 689 | M33 = M[:3, :3] 690 | # normal: cross independent eigenvectors corresponding to the eigenvalue 1 691 | w, V = numpy.linalg.eig(M33) 692 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-4)[0] 693 | if len(i) < 2: 694 | raise ValueError("no two linear independent eigenvectors found %s" % w) 695 | V = numpy.real(V[:, i]).squeeze().T 696 | lenorm = -1.0 697 | for i0, i1 in ((0, 1), (0, 2), (1, 2)): 698 | n = numpy.cross(V[i0], V[i1]) 699 | w = vector_norm(n) 700 | if w > lenorm: 701 | lenorm = w 702 | normal = n 703 | normal /= lenorm 704 | # direction and angle 705 | direction = numpy.dot(M33 - numpy.identity(3), normal) 706 | angle = vector_norm(direction) 707 | direction /= angle 708 | angle = math.atan(angle) 709 | # point: eigenvector corresponding to eigenvalue 1 710 | w, V = numpy.linalg.eig(M) 711 | i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] 712 | if not len(i): 713 | raise ValueError("no eigenvector corresponding to eigenvalue 1") 714 | point = numpy.real(V[:, i[-1]]).squeeze() 715 | point /= point[3] 716 | return angle, direction, point, normal 717 | 718 | 719 | def decompose_matrix(matrix): 720 | """Return sequence of transformations from transformation matrix. 721 | 722 | matrix : array_like 723 | Non-degenerative homogeneous transformation matrix 724 | 725 | Return tuple of: 726 | scale : vector of 3 scaling factors 727 | shear : list of shear factors for x-y, x-z, y-z axes 728 | angles : list of Euler angles about static x, y, z axes 729 | translate : translation vector along x, y, z axes 730 | perspective : perspective partition of matrix 731 | 732 | Raise ValueError if matrix is of wrong type or degenerative. 733 | 734 | >>> T0 = translation_matrix([1, 2, 3]) 735 | >>> scale, shear, angles, trans, persp = decompose_matrix(T0) 736 | >>> T1 = translation_matrix(trans) 737 | >>> numpy.allclose(T0, T1) 738 | True 739 | >>> S = scale_matrix(0.123) 740 | >>> scale, shear, angles, trans, persp = decompose_matrix(S) 741 | >>> scale[0] 742 | 0.123 743 | >>> R0 = euler_matrix(1, 2, 3) 744 | >>> scale, shear, angles, trans, persp = decompose_matrix(R0) 745 | >>> R1 = euler_matrix(*angles) 746 | >>> numpy.allclose(R0, R1) 747 | True 748 | 749 | """ 750 | M = numpy.array(matrix, dtype=numpy.float64, copy=True).T 751 | if abs(M[3, 3]) < _EPS: 752 | raise ValueError("M[3, 3] is zero") 753 | M /= M[3, 3] 754 | P = M.copy() 755 | P[:, 3] = 0.0, 0.0, 0.0, 1.0 756 | if not numpy.linalg.det(P): 757 | raise ValueError("matrix is singular") 758 | 759 | scale = numpy.zeros((3, )) 760 | shear = [0.0, 0.0, 0.0] 761 | angles = [0.0, 0.0, 0.0] 762 | 763 | if any(abs(M[:3, 3]) > _EPS): 764 | perspective = numpy.dot(M[:, 3], numpy.linalg.inv(P.T)) 765 | M[:, 3] = 0.0, 0.0, 0.0, 1.0 766 | else: 767 | perspective = numpy.array([0.0, 0.0, 0.0, 1.0]) 768 | 769 | translate = M[3, :3].copy() 770 | M[3, :3] = 0.0 771 | 772 | row = M[:3, :3].copy() 773 | scale[0] = vector_norm(row[0]) 774 | row[0] /= scale[0] 775 | shear[0] = numpy.dot(row[0], row[1]) 776 | row[1] -= row[0] * shear[0] 777 | scale[1] = vector_norm(row[1]) 778 | row[1] /= scale[1] 779 | shear[0] /= scale[1] 780 | shear[1] = numpy.dot(row[0], row[2]) 781 | row[2] -= row[0] * shear[1] 782 | shear[2] = numpy.dot(row[1], row[2]) 783 | row[2] -= row[1] * shear[2] 784 | scale[2] = vector_norm(row[2]) 785 | row[2] /= scale[2] 786 | shear[1:] /= scale[2] 787 | 788 | if numpy.dot(row[0], numpy.cross(row[1], row[2])) < 0: 789 | numpy.negative(scale, scale) 790 | numpy.negative(row, row) 791 | 792 | angles[1] = math.asin(-row[0, 2]) 793 | if math.cos(angles[1]): 794 | angles[0] = math.atan2(row[1, 2], row[2, 2]) 795 | angles[2] = math.atan2(row[0, 1], row[0, 0]) 796 | else: 797 | #angles[0] = math.atan2(row[1, 0], row[1, 1]) 798 | angles[0] = math.atan2(-row[2, 1], row[1, 1]) 799 | angles[2] = 0.0 800 | 801 | return scale, shear, angles, translate, perspective 802 | 803 | 804 | def compose_matrix(scale=None, shear=None, angles=None, translate=None, 805 | perspective=None): 806 | """Return transformation matrix from sequence of transformations. 807 | 808 | This is the inverse of the decompose_matrix function. 809 | 810 | Sequence of transformations: 811 | scale : vector of 3 scaling factors 812 | shear : list of shear factors for x-y, x-z, y-z axes 813 | angles : list of Euler angles about static x, y, z axes 814 | translate : translation vector along x, y, z axes 815 | perspective : perspective partition of matrix 816 | 817 | >>> scale = numpy.random.random(3) - 0.5 818 | >>> shear = numpy.random.random(3) - 0.5 819 | >>> angles = (numpy.random.random(3) - 0.5) * (2*math.pi) 820 | >>> trans = numpy.random.random(3) - 0.5 821 | >>> persp = numpy.random.random(4) - 0.5 822 | >>> M0 = compose_matrix(scale, shear, angles, trans, persp) 823 | >>> result = decompose_matrix(M0) 824 | >>> M1 = compose_matrix(*result) 825 | >>> is_same_transform(M0, M1) 826 | True 827 | 828 | """ 829 | M = numpy.identity(4) 830 | if perspective is not None: 831 | P = numpy.identity(4) 832 | P[3, :] = perspective[:4] 833 | M = numpy.dot(M, P) 834 | if translate is not None: 835 | T = numpy.identity(4) 836 | T[:3, 3] = translate[:3] 837 | M = numpy.dot(M, T) 838 | if angles is not None: 839 | R = euler_matrix(angles[0], angles[1], angles[2], 'sxyz') 840 | M = numpy.dot(M, R) 841 | if shear is not None: 842 | Z = numpy.identity(4) 843 | Z[1, 2] = shear[2] 844 | Z[0, 2] = shear[1] 845 | Z[0, 1] = shear[0] 846 | M = numpy.dot(M, Z) 847 | if scale is not None: 848 | S = numpy.identity(4) 849 | S[0, 0] = scale[0] 850 | S[1, 1] = scale[1] 851 | S[2, 2] = scale[2] 852 | M = numpy.dot(M, S) 853 | M /= M[3, 3] 854 | return M 855 | 856 | 857 | def orthogonalization_matrix(lengths, angles): 858 | """Return orthogonalization matrix for crystallographic cell coordinates. 859 | 860 | Angles are expected in degrees. 861 | 862 | The de-orthogonalization matrix is the inverse. 863 | 864 | >>> O = orthogonalization_matrix([10, 10, 10], [90, 90, 90]) 865 | >>> numpy.allclose(O[:3, :3], numpy.identity(3, float) * 10) 866 | True 867 | >>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7]) 868 | >>> numpy.allclose(numpy.sum(O), 43.063229) 869 | True 870 | 871 | """ 872 | a, b, c = lengths 873 | angles = numpy.radians(angles) 874 | sina, sinb, _ = numpy.sin(angles) 875 | cosa, cosb, cosg = numpy.cos(angles) 876 | co = (cosa * cosb - cosg) / (sina * sinb) 877 | return numpy.array([ 878 | [ a*sinb*math.sqrt(1.0-co*co), 0.0, 0.0, 0.0], 879 | [-a*sinb*co, b*sina, 0.0, 0.0], 880 | [ a*cosb, b*cosa, c, 0.0], 881 | [ 0.0, 0.0, 0.0, 1.0]]) 882 | 883 | 884 | def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True): 885 | """Return affine transform matrix to register two point sets. 886 | 887 | v0 and v1 are shape (ndims, \*) arrays of at least ndims non-homogeneous 888 | coordinates, where ndims is the dimensionality of the coordinate space. 889 | 890 | If shear is False, a similarity transformation matrix is returned. 891 | If also scale is False, a rigid/Euclidean transformation matrix 892 | is returned. 893 | 894 | By default the algorithm by Hartley and Zissermann [15] is used. 895 | If usesvd is True, similarity and Euclidean transformation matrices 896 | are calculated by minimizing the weighted sum of squared deviations 897 | (RMSD) according to the algorithm by Kabsch [8]. 898 | Otherwise, and if ndims is 3, the quaternion based algorithm by Horn [9] 899 | is used, which is slower when using this Python implementation. 900 | 901 | The returned matrix performs rotation, translation and uniform scaling 902 | (if specified). 903 | 904 | >>> v0 = [[0, 1031, 1031, 0], [0, 0, 1600, 1600]] 905 | >>> v1 = [[675, 826, 826, 677], [55, 52, 281, 277]] 906 | >>> affine_matrix_from_points(v0, v1) 907 | array([[ 0.14549, 0.00062, 675.50008], 908 | [ 0.00048, 0.14094, 53.24971], 909 | [ 0. , 0. , 1. ]]) 910 | >>> T = translation_matrix(numpy.random.random(3)-0.5) 911 | >>> R = random_rotation_matrix(numpy.random.random(3)) 912 | >>> S = scale_matrix(random.random()) 913 | >>> M = concatenate_matrices(T, R, S) 914 | >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20 915 | >>> v0[3] = 1 916 | >>> v1 = numpy.dot(M, v0) 917 | >>> v0[:3] += numpy.random.normal(0, 1e-8, 300).reshape(3, -1) 918 | >>> M = affine_matrix_from_points(v0[:3], v1[:3]) 919 | >>> numpy.allclose(v1, numpy.dot(M, v0)) 920 | True 921 | 922 | More examples in superimposition_matrix() 923 | 924 | """ 925 | v0 = numpy.array(v0, dtype=numpy.float64, copy=True) 926 | v1 = numpy.array(v1, dtype=numpy.float64, copy=True) 927 | 928 | ndims = v0.shape[0] 929 | if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape: 930 | raise ValueError("input arrays are of wrong shape or type") 931 | 932 | # move centroids to origin 933 | t0 = -numpy.mean(v0, axis=1) 934 | M0 = numpy.identity(ndims+1) 935 | M0[:ndims, ndims] = t0 936 | v0 += t0.reshape(ndims, 1) 937 | t1 = -numpy.mean(v1, axis=1) 938 | M1 = numpy.identity(ndims+1) 939 | M1[:ndims, ndims] = t1 940 | v1 += t1.reshape(ndims, 1) 941 | 942 | if shear: 943 | # Affine transformation 944 | A = numpy.concatenate((v0, v1), axis=0) 945 | u, s, vh = numpy.linalg.svd(A.T) 946 | vh = vh[:ndims].T 947 | B = vh[:ndims] 948 | C = vh[ndims:2*ndims] 949 | t = numpy.dot(C, numpy.linalg.pinv(B)) 950 | t = numpy.concatenate((t, numpy.zeros((ndims, 1))), axis=1) 951 | M = numpy.vstack((t, ((0.0,)*ndims) + (1.0,))) 952 | elif usesvd or ndims != 3: 953 | # Rigid transformation via SVD of covariance matrix 954 | u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T)) 955 | # rotation matrix from SVD orthonormal bases 956 | R = numpy.dot(u, vh) 957 | if numpy.linalg.det(R) < 0.0: 958 | # R does not constitute right handed system 959 | R -= numpy.outer(u[:, ndims-1], vh[ndims-1, :]*2.0) 960 | s[-1] *= -1.0 961 | # homogeneous transformation matrix 962 | M = numpy.identity(ndims+1) 963 | M[:ndims, :ndims] = R 964 | else: 965 | # Rigid transformation matrix via quaternion 966 | # compute symmetric matrix N 967 | xx, yy, zz = numpy.sum(v0 * v1, axis=1) 968 | xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1) 969 | xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1) 970 | N = [[xx+yy+zz, 0.0, 0.0, 0.0], 971 | [yz-zy, xx-yy-zz, 0.0, 0.0], 972 | [zx-xz, xy+yx, yy-xx-zz, 0.0], 973 | [xy-yx, zx+xz, yz+zy, zz-xx-yy]] 974 | # quaternion: eigenvector corresponding to most positive eigenvalue 975 | w, V = numpy.linalg.eigh(N) 976 | q = V[:, numpy.argmax(w)] 977 | q /= vector_norm(q) # unit quaternion 978 | # homogeneous transformation matrix 979 | M = quaternion_matrix(q) 980 | 981 | if scale and not shear: 982 | # Affine transformation; scale is ratio of RMS deviations from centroid 983 | v0 *= v0 984 | v1 *= v1 985 | M[:ndims, :ndims] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0)) 986 | 987 | # move centroids back 988 | M = numpy.dot(numpy.linalg.inv(M1), numpy.dot(M, M0)) 989 | M /= M[ndims, ndims] 990 | return M 991 | 992 | 993 | def superimposition_matrix(v0, v1, scale=False, usesvd=True): 994 | """Return matrix to transform given 3D point set into second point set. 995 | 996 | v0 and v1 are shape (3, \*) or (4, \*) arrays of at least 3 points. 997 | 998 | The parameters scale and usesvd are explained in the more general 999 | affine_matrix_from_points function. 1000 | 1001 | The returned matrix is a similarity or Euclidean transformation matrix. 1002 | This function has a fast C implementation in transformations.c. 1003 | 1004 | >>> v0 = numpy.random.rand(3, 10) 1005 | >>> M = superimposition_matrix(v0, v0) 1006 | >>> numpy.allclose(M, numpy.identity(4)) 1007 | True 1008 | >>> R = random_rotation_matrix(numpy.random.random(3)) 1009 | >>> v0 = [[1,0,0], [0,1,0], [0,0,1], [1,1,1]] 1010 | >>> v1 = numpy.dot(R, v0) 1011 | >>> M = superimposition_matrix(v0, v1) 1012 | >>> numpy.allclose(v1, numpy.dot(M, v0)) 1013 | True 1014 | >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20 1015 | >>> v0[3] = 1 1016 | >>> v1 = numpy.dot(R, v0) 1017 | >>> M = superimposition_matrix(v0, v1) 1018 | >>> numpy.allclose(v1, numpy.dot(M, v0)) 1019 | True 1020 | >>> S = scale_matrix(random.random()) 1021 | >>> T = translation_matrix(numpy.random.random(3)-0.5) 1022 | >>> M = concatenate_matrices(T, R, S) 1023 | >>> v1 = numpy.dot(M, v0) 1024 | >>> v0[:3] += numpy.random.normal(0, 1e-9, 300).reshape(3, -1) 1025 | >>> M = superimposition_matrix(v0, v1, scale=True) 1026 | >>> numpy.allclose(v1, numpy.dot(M, v0)) 1027 | True 1028 | >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False) 1029 | >>> numpy.allclose(v1, numpy.dot(M, v0)) 1030 | True 1031 | >>> v = numpy.empty((4, 100, 3)) 1032 | >>> v[:, :, 0] = v0 1033 | >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False) 1034 | >>> numpy.allclose(v1, numpy.dot(M, v[:, :, 0])) 1035 | True 1036 | 1037 | """ 1038 | v0 = numpy.array(v0, dtype=numpy.float64, copy=False)[:3] 1039 | v1 = numpy.array(v1, dtype=numpy.float64, copy=False)[:3] 1040 | return affine_matrix_from_points(v0, v1, shear=False, 1041 | scale=scale, usesvd=usesvd) 1042 | 1043 | 1044 | def euler_matrix(ai, aj, ak, axes='sxyz'): 1045 | """Return homogeneous rotation matrix from Euler angles and axis sequence. 1046 | 1047 | ai, aj, ak : Euler's roll, pitch and yaw angles 1048 | axes : One of 24 axis sequences as string or encoded tuple 1049 | 1050 | >>> R = euler_matrix(1, 2, 3, 'syxz') 1051 | >>> numpy.allclose(numpy.sum(R[0]), -1.34786452) 1052 | True 1053 | >>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1)) 1054 | >>> numpy.allclose(numpy.sum(R[0]), -0.383436184) 1055 | True 1056 | >>> ai, aj, ak = (4*math.pi) * (numpy.random.random(3) - 0.5) 1057 | >>> for axes in _AXES2TUPLE.keys(): 1058 | ... R = euler_matrix(ai, aj, ak, axes) 1059 | >>> for axes in _TUPLE2AXES.keys(): 1060 | ... R = euler_matrix(ai, aj, ak, axes) 1061 | 1062 | """ 1063 | try: 1064 | firstaxis, parity, repetition, frame = _AXES2TUPLE[axes] 1065 | except (AttributeError, KeyError): 1066 | _TUPLE2AXES[axes] # validation 1067 | firstaxis, parity, repetition, frame = axes 1068 | 1069 | i = firstaxis 1070 | j = _NEXT_AXIS[i+parity] 1071 | k = _NEXT_AXIS[i-parity+1] 1072 | 1073 | if frame: 1074 | ai, ak = ak, ai 1075 | if parity: 1076 | ai, aj, ak = -ai, -aj, -ak 1077 | 1078 | si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak) 1079 | ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak) 1080 | cc, cs = ci*ck, ci*sk 1081 | sc, ss = si*ck, si*sk 1082 | 1083 | M = numpy.identity(4) 1084 | if repetition: 1085 | M[i, i] = cj 1086 | M[i, j] = sj*si 1087 | M[i, k] = sj*ci 1088 | M[j, i] = sj*sk 1089 | M[j, j] = -cj*ss+cc 1090 | M[j, k] = -cj*cs-sc 1091 | M[k, i] = -sj*ck 1092 | M[k, j] = cj*sc+cs 1093 | M[k, k] = cj*cc-ss 1094 | else: 1095 | M[i, i] = cj*ck 1096 | M[i, j] = sj*sc-cs 1097 | M[i, k] = sj*cc+ss 1098 | M[j, i] = cj*sk 1099 | M[j, j] = sj*ss+cc 1100 | M[j, k] = sj*cs-sc 1101 | M[k, i] = -sj 1102 | M[k, j] = cj*si 1103 | M[k, k] = cj*ci 1104 | return M 1105 | 1106 | 1107 | def euler_from_matrix(matrix, axes='sxyz'): 1108 | """Return Euler angles from rotation matrix for specified axis sequence. 1109 | 1110 | axes : One of 24 axis sequences as string or encoded tuple 1111 | 1112 | Note that many Euler angle triplets can describe one matrix. 1113 | 1114 | >>> R0 = euler_matrix(1, 2, 3, 'syxz') 1115 | >>> al, be, ga = euler_from_matrix(R0, 'syxz') 1116 | >>> R1 = euler_matrix(al, be, ga, 'syxz') 1117 | >>> numpy.allclose(R0, R1) 1118 | True 1119 | >>> angles = (4*math.pi) * (numpy.random.random(3) - 0.5) 1120 | >>> for axes in _AXES2TUPLE.keys(): 1121 | ... R0 = euler_matrix(axes=axes, *angles) 1122 | ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes)) 1123 | ... if not numpy.allclose(R0, R1): print(axes, "failed") 1124 | 1125 | """ 1126 | try: 1127 | firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()] 1128 | except (AttributeError, KeyError): 1129 | _TUPLE2AXES[axes] # validation 1130 | firstaxis, parity, repetition, frame = axes 1131 | 1132 | i = firstaxis 1133 | j = _NEXT_AXIS[i+parity] 1134 | k = _NEXT_AXIS[i-parity+1] 1135 | 1136 | M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:3, :3] 1137 | if repetition: 1138 | sy = math.sqrt(M[i, j]*M[i, j] + M[i, k]*M[i, k]) 1139 | if sy > _EPS: 1140 | ax = math.atan2( M[i, j], M[i, k]) 1141 | ay = math.atan2( sy, M[i, i]) 1142 | az = math.atan2( M[j, i], -M[k, i]) 1143 | else: 1144 | ax = math.atan2(-M[j, k], M[j, j]) 1145 | ay = math.atan2( sy, M[i, i]) 1146 | az = 0.0 1147 | else: 1148 | cy = math.sqrt(M[i, i]*M[i, i] + M[j, i]*M[j, i]) 1149 | if cy > _EPS: 1150 | ax = math.atan2( M[k, j], M[k, k]) 1151 | ay = math.atan2(-M[k, i], cy) 1152 | az = math.atan2( M[j, i], M[i, i]) 1153 | else: 1154 | ax = math.atan2(-M[j, k], M[j, j]) 1155 | ay = math.atan2(-M[k, i], cy) 1156 | az = 0.0 1157 | 1158 | if parity: 1159 | ax, ay, az = -ax, -ay, -az 1160 | if frame: 1161 | ax, az = az, ax 1162 | return ax, ay, az 1163 | 1164 | 1165 | def euler_from_quaternion(quaternion, axes='sxyz'): 1166 | """Return Euler angles from quaternion for specified axis sequence. 1167 | 1168 | >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0]) 1169 | >>> numpy.allclose(angles, [0.123, 0, 0]) 1170 | True 1171 | 1172 | """ 1173 | return euler_from_matrix(quaternion_matrix(quaternion), axes) 1174 | 1175 | 1176 | def quaternion_from_euler(ai, aj, ak, axes='sxyz'): 1177 | """Return quaternion from Euler angles and axis sequence. 1178 | 1179 | ai, aj, ak : Euler's roll, pitch and yaw angles 1180 | axes : One of 24 axis sequences as string or encoded tuple 1181 | 1182 | >>> q = quaternion_from_euler(1, 2, 3, 'ryxz') 1183 | >>> numpy.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435]) 1184 | True 1185 | 1186 | """ 1187 | try: 1188 | firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()] 1189 | except (AttributeError, KeyError): 1190 | _TUPLE2AXES[axes] # validation 1191 | firstaxis, parity, repetition, frame = axes 1192 | 1193 | i = firstaxis + 1 1194 | j = _NEXT_AXIS[i+parity-1] + 1 1195 | k = _NEXT_AXIS[i-parity] + 1 1196 | 1197 | if frame: 1198 | ai, ak = ak, ai 1199 | if parity: 1200 | aj = -aj 1201 | 1202 | ai /= 2.0 1203 | aj /= 2.0 1204 | ak /= 2.0 1205 | ci = math.cos(ai) 1206 | si = math.sin(ai) 1207 | cj = math.cos(aj) 1208 | sj = math.sin(aj) 1209 | ck = math.cos(ak) 1210 | sk = math.sin(ak) 1211 | cc = ci*ck 1212 | cs = ci*sk 1213 | sc = si*ck 1214 | ss = si*sk 1215 | 1216 | q = numpy.empty((4, )) 1217 | if repetition: 1218 | q[0] = cj*(cc - ss) 1219 | q[i] = cj*(cs + sc) 1220 | q[j] = sj*(cc + ss) 1221 | q[k] = sj*(cs - sc) 1222 | else: 1223 | q[0] = cj*cc + sj*ss 1224 | q[i] = cj*sc - sj*cs 1225 | q[j] = cj*ss + sj*cc 1226 | q[k] = cj*cs - sj*sc 1227 | if parity: 1228 | q[j] *= -1.0 1229 | 1230 | return q 1231 | 1232 | 1233 | def quaternion_about_axis(angle, axis): 1234 | """Return quaternion for rotation about axis. 1235 | 1236 | >>> q = quaternion_about_axis(0.123, [1, 0, 0]) 1237 | >>> numpy.allclose(q, [0.99810947, 0.06146124, 0, 0]) 1238 | True 1239 | 1240 | """ 1241 | q = numpy.array([0.0, axis[0], axis[1], axis[2]]) 1242 | qlen = vector_norm(q) 1243 | if qlen > _EPS: 1244 | q *= math.sin(angle/2.0) / qlen 1245 | q[0] = math.cos(angle/2.0) 1246 | return q 1247 | 1248 | 1249 | def quaternion_matrix(quaternion): 1250 | """Return homogeneous rotation matrix from quaternion. 1251 | 1252 | >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0]) 1253 | >>> numpy.allclose(M, rotation_matrix(0.123, [1, 0, 0])) 1254 | True 1255 | >>> M = quaternion_matrix([1, 0, 0, 0]) 1256 | >>> numpy.allclose(M, numpy.identity(4)) 1257 | True 1258 | >>> M = quaternion_matrix([0, 1, 0, 0]) 1259 | >>> numpy.allclose(M, numpy.diag([1, -1, -1, 1])) 1260 | True 1261 | 1262 | """ 1263 | q = numpy.array(quaternion, dtype=numpy.float64, copy=True) 1264 | n = numpy.dot(q, q) 1265 | if n < _EPS: 1266 | return numpy.identity(4) 1267 | q *= math.sqrt(2.0 / n) 1268 | q = numpy.outer(q, q) 1269 | return numpy.array([ 1270 | [1.0-q[2, 2]-q[3, 3], q[1, 2]-q[3, 0], q[1, 3]+q[2, 0], 0.0], 1271 | [ q[1, 2]+q[3, 0], 1.0-q[1, 1]-q[3, 3], q[2, 3]-q[1, 0], 0.0], 1272 | [ q[1, 3]-q[2, 0], q[2, 3]+q[1, 0], 1.0-q[1, 1]-q[2, 2], 0.0], 1273 | [ 0.0, 0.0, 0.0, 1.0]]) 1274 | 1275 | 1276 | def quaternion_from_matrix(matrix, isprecise=False): 1277 | """Return quaternion from rotation matrix. 1278 | 1279 | If isprecise is True, the input matrix is assumed to be a precise rotation 1280 | matrix and a faster algorithm is used. 1281 | 1282 | >>> q = quaternion_from_matrix(numpy.identity(4), True) 1283 | >>> numpy.allclose(q, [1, 0, 0, 0]) 1284 | True 1285 | >>> q = quaternion_from_matrix(numpy.diag([1, -1, -1, 1])) 1286 | >>> numpy.allclose(q, [0, 1, 0, 0]) or numpy.allclose(q, [0, -1, 0, 0]) 1287 | True 1288 | >>> R = rotation_matrix(0.123, (1, 2, 3)) 1289 | >>> q = quaternion_from_matrix(R, True) 1290 | >>> numpy.allclose(q, [0.9981095, 0.0164262, 0.0328524, 0.0492786]) 1291 | True 1292 | >>> R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0], 1293 | ... [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]] 1294 | >>> q = quaternion_from_matrix(R) 1295 | >>> numpy.allclose(q, [0.19069, 0.43736, 0.87485, -0.083611]) 1296 | True 1297 | >>> R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0], 1298 | ... [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]] 1299 | >>> q = quaternion_from_matrix(R) 1300 | >>> numpy.allclose(q, [0.82336615, -0.13610694, 0.46344705, -0.29792603]) 1301 | True 1302 | >>> R = random_rotation_matrix() 1303 | >>> q = quaternion_from_matrix(R) 1304 | >>> is_same_transform(R, quaternion_matrix(q)) 1305 | True 1306 | 1307 | """ 1308 | M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:4, :4] 1309 | if isprecise: 1310 | q = numpy.empty((4, )) 1311 | t = numpy.trace(M) 1312 | if t > M[3, 3]: 1313 | q[0] = t 1314 | q[3] = M[1, 0] - M[0, 1] 1315 | q[2] = M[0, 2] - M[2, 0] 1316 | q[1] = M[2, 1] - M[1, 2] 1317 | else: 1318 | i, j, k = 1, 2, 3 1319 | if M[1, 1] > M[0, 0]: 1320 | i, j, k = 2, 3, 1 1321 | if M[2, 2] > M[i, i]: 1322 | i, j, k = 3, 1, 2 1323 | t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3] 1324 | q[i] = t 1325 | q[j] = M[i, j] + M[j, i] 1326 | q[k] = M[k, i] + M[i, k] 1327 | q[3] = M[k, j] - M[j, k] 1328 | q *= 0.5 / math.sqrt(t * M[3, 3]) 1329 | else: 1330 | m00 = M[0, 0] 1331 | m01 = M[0, 1] 1332 | m02 = M[0, 2] 1333 | m10 = M[1, 0] 1334 | m11 = M[1, 1] 1335 | m12 = M[1, 2] 1336 | m20 = M[2, 0] 1337 | m21 = M[2, 1] 1338 | m22 = M[2, 2] 1339 | # symmetric matrix K 1340 | K = numpy.array([[m00-m11-m22, 0.0, 0.0, 0.0], 1341 | [m01+m10, m11-m00-m22, 0.0, 0.0], 1342 | [m02+m20, m12+m21, m22-m00-m11, 0.0], 1343 | [m21-m12, m02-m20, m10-m01, m00+m11+m22]]) 1344 | K /= 3.0 1345 | # quaternion is eigenvector of K that corresponds to largest eigenvalue 1346 | w, V = numpy.linalg.eigh(K) 1347 | q = V[[3, 0, 1, 2], numpy.argmax(w)] 1348 | if q[0] < 0.0: 1349 | numpy.negative(q, q) 1350 | return q 1351 | 1352 | 1353 | def quaternion_multiply(quaternion1, quaternion0): 1354 | """Return multiplication of two quaternions. 1355 | 1356 | >>> q = quaternion_multiply([4, 1, -2, 3], [8, -5, 6, 7]) 1357 | >>> numpy.allclose(q, [28, -44, -14, 48]) 1358 | True 1359 | 1360 | """ 1361 | w0, x0, y0, z0 = quaternion0 1362 | w1, x1, y1, z1 = quaternion1 1363 | return numpy.array([-x1*x0 - y1*y0 - z1*z0 + w1*w0, 1364 | x1*w0 + y1*z0 - z1*y0 + w1*x0, 1365 | -x1*z0 + y1*w0 + z1*x0 + w1*y0, 1366 | x1*y0 - y1*x0 + z1*w0 + w1*z0], dtype=numpy.float64) 1367 | 1368 | 1369 | def quaternion_conjugate(quaternion): 1370 | """Return conjugate of quaternion. 1371 | 1372 | >>> q0 = random_quaternion() 1373 | >>> q1 = quaternion_conjugate(q0) 1374 | >>> q1[0] == q0[0] and all(q1[1:] == -q0[1:]) 1375 | True 1376 | 1377 | """ 1378 | q = numpy.array(quaternion, dtype=numpy.float64, copy=True) 1379 | numpy.negative(q[1:], q[1:]) 1380 | return q 1381 | 1382 | 1383 | def quaternion_inverse(quaternion): 1384 | """Return inverse of quaternion. 1385 | 1386 | >>> q0 = random_quaternion() 1387 | >>> q1 = quaternion_inverse(q0) 1388 | >>> numpy.allclose(quaternion_multiply(q0, q1), [1, 0, 0, 0]) 1389 | True 1390 | 1391 | """ 1392 | q = numpy.array(quaternion, dtype=numpy.float64, copy=True) 1393 | numpy.negative(q[1:], q[1:]) 1394 | return q / numpy.dot(q, q) 1395 | 1396 | 1397 | def quaternion_real(quaternion): 1398 | """Return real part of quaternion. 1399 | 1400 | >>> quaternion_real([3, 0, 1, 2]) 1401 | 3.0 1402 | 1403 | """ 1404 | return float(quaternion[0]) 1405 | 1406 | 1407 | def quaternion_imag(quaternion): 1408 | """Return imaginary part of quaternion. 1409 | 1410 | >>> quaternion_imag([3, 0, 1, 2]) 1411 | array([ 0., 1., 2.]) 1412 | 1413 | """ 1414 | return numpy.array(quaternion[1:4], dtype=numpy.float64, copy=True) 1415 | 1416 | 1417 | def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True): 1418 | """Return spherical linear interpolation between two quaternions. 1419 | 1420 | >>> q0 = random_quaternion() 1421 | >>> q1 = random_quaternion() 1422 | >>> q = quaternion_slerp(q0, q1, 0) 1423 | >>> numpy.allclose(q, q0) 1424 | True 1425 | >>> q = quaternion_slerp(q0, q1, 1, 1) 1426 | >>> numpy.allclose(q, q1) 1427 | True 1428 | >>> q = quaternion_slerp(q0, q1, 0.5) 1429 | >>> angle = math.acos(numpy.dot(q0, q)) 1430 | >>> numpy.allclose(2, math.acos(numpy.dot(q0, q1)) / angle) or \ 1431 | numpy.allclose(2, math.acos(-numpy.dot(q0, q1)) / angle) 1432 | True 1433 | 1434 | """ 1435 | q0 = unit_vector(quat0[:4]) 1436 | q1 = unit_vector(quat1[:4]) 1437 | if fraction == 0.0: 1438 | return q0 1439 | elif fraction == 1.0: 1440 | return q1 1441 | d = numpy.dot(q0, q1) 1442 | if abs(abs(d) - 1.0) < _EPS: 1443 | return q0 1444 | if shortestpath and d < 0.0: 1445 | # invert rotation 1446 | d = -d 1447 | numpy.negative(q1, q1) 1448 | angle = math.acos(d) + spin * math.pi 1449 | if abs(angle) < _EPS: 1450 | return q0 1451 | isin = 1.0 / math.sin(angle) 1452 | q0 *= math.sin((1.0 - fraction) * angle) * isin 1453 | q1 *= math.sin(fraction * angle) * isin 1454 | q0 += q1 1455 | return q0 1456 | 1457 | 1458 | def random_quaternion(rand=None): 1459 | """Return uniform random unit quaternion. 1460 | 1461 | rand: array like or None 1462 | Three independent random variables that are uniformly distributed 1463 | between 0 and 1. 1464 | 1465 | >>> q = random_quaternion() 1466 | >>> numpy.allclose(1, vector_norm(q)) 1467 | True 1468 | >>> q = random_quaternion(numpy.random.random(3)) 1469 | >>> len(q.shape), q.shape[0]==4 1470 | (1, True) 1471 | 1472 | """ 1473 | if rand is None: 1474 | rand = numpy.random.rand(3) 1475 | else: 1476 | assert len(rand) == 3 1477 | r1 = numpy.sqrt(1.0 - rand[0]) 1478 | r2 = numpy.sqrt(rand[0]) 1479 | pi2 = math.pi * 2.0 1480 | t1 = pi2 * rand[1] 1481 | t2 = pi2 * rand[2] 1482 | return numpy.array([numpy.cos(t2)*r2, numpy.sin(t1)*r1, 1483 | numpy.cos(t1)*r1, numpy.sin(t2)*r2]) 1484 | 1485 | 1486 | def random_rotation_matrix(rand=None): 1487 | """Return uniform random rotation matrix. 1488 | 1489 | rand: array like 1490 | Three independent random variables that are uniformly distributed 1491 | between 0 and 1 for each returned quaternion. 1492 | 1493 | >>> R = random_rotation_matrix() 1494 | >>> numpy.allclose(numpy.dot(R.T, R), numpy.identity(4)) 1495 | True 1496 | 1497 | """ 1498 | return quaternion_matrix(random_quaternion(rand)) 1499 | 1500 | 1501 | class Arcball(object): 1502 | """Virtual Trackball Control. 1503 | 1504 | >>> ball = Arcball() 1505 | >>> ball = Arcball(initial=numpy.identity(4)) 1506 | >>> ball.place([320, 320], 320) 1507 | >>> ball.down([500, 250]) 1508 | >>> ball.drag([475, 275]) 1509 | >>> R = ball.matrix() 1510 | >>> numpy.allclose(numpy.sum(R), 3.90583455) 1511 | True 1512 | >>> ball = Arcball(initial=[1, 0, 0, 0]) 1513 | >>> ball.place([320, 320], 320) 1514 | >>> ball.setaxes([1, 1, 0], [-1, 1, 0]) 1515 | >>> ball.constrain = True 1516 | >>> ball.down([400, 200]) 1517 | >>> ball.drag([200, 400]) 1518 | >>> R = ball.matrix() 1519 | >>> numpy.allclose(numpy.sum(R), 0.2055924) 1520 | True 1521 | >>> ball.next() 1522 | 1523 | """ 1524 | def __init__(self, initial=None): 1525 | """Initialize virtual trackball control. 1526 | 1527 | initial : quaternion or rotation matrix 1528 | 1529 | """ 1530 | self._axis = None 1531 | self._axes = None 1532 | self._radius = 1.0 1533 | self._center = [0.0, 0.0] 1534 | self._vdown = numpy.array([0.0, 0.0, 1.0]) 1535 | self._constrain = False 1536 | if initial is None: 1537 | self._qdown = numpy.array([1.0, 0.0, 0.0, 0.0]) 1538 | else: 1539 | initial = numpy.array(initial, dtype=numpy.float64) 1540 | if initial.shape == (4, 4): 1541 | self._qdown = quaternion_from_matrix(initial) 1542 | elif initial.shape == (4, ): 1543 | initial /= vector_norm(initial) 1544 | self._qdown = initial 1545 | else: 1546 | raise ValueError("initial not a quaternion or matrix") 1547 | self._qnow = self._qpre = self._qdown 1548 | 1549 | def place(self, center, radius): 1550 | """Place Arcball, e.g. when window size changes. 1551 | 1552 | center : sequence[2] 1553 | Window coordinates of trackball center. 1554 | radius : float 1555 | Radius of trackball in window coordinates. 1556 | 1557 | """ 1558 | self._radius = float(radius) 1559 | self._center[0] = center[0] 1560 | self._center[1] = center[1] 1561 | 1562 | def setaxes(self, *axes): 1563 | """Set axes to constrain rotations.""" 1564 | if axes is None: 1565 | self._axes = None 1566 | else: 1567 | self._axes = [unit_vector(axis) for axis in axes] 1568 | 1569 | @property 1570 | def constrain(self): 1571 | """Return state of constrain to axis mode.""" 1572 | return self._constrain 1573 | 1574 | @constrain.setter 1575 | def constrain(self, value): 1576 | """Set state of constrain to axis mode.""" 1577 | self._constrain = bool(value) 1578 | 1579 | def down(self, point): 1580 | """Set initial cursor window coordinates and pick constrain-axis.""" 1581 | self._vdown = arcball_map_to_sphere(point, self._center, self._radius) 1582 | self._qdown = self._qpre = self._qnow 1583 | if self._constrain and self._axes is not None: 1584 | self._axis = arcball_nearest_axis(self._vdown, self._axes) 1585 | self._vdown = arcball_constrain_to_axis(self._vdown, self._axis) 1586 | else: 1587 | self._axis = None 1588 | 1589 | def drag(self, point): 1590 | """Update current cursor window coordinates.""" 1591 | vnow = arcball_map_to_sphere(point, self._center, self._radius) 1592 | if self._axis is not None: 1593 | vnow = arcball_constrain_to_axis(vnow, self._axis) 1594 | self._qpre = self._qnow 1595 | t = numpy.cross(self._vdown, vnow) 1596 | if numpy.dot(t, t) < _EPS: 1597 | self._qnow = self._qdown 1598 | else: 1599 | q = [numpy.dot(self._vdown, vnow), t[0], t[1], t[2]] 1600 | self._qnow = quaternion_multiply(q, self._qdown) 1601 | 1602 | def next(self, acceleration=0.0): 1603 | """Continue rotation in direction of last drag.""" 1604 | q = quaternion_slerp(self._qpre, self._qnow, 2.0+acceleration, False) 1605 | self._qpre, self._qnow = self._qnow, q 1606 | 1607 | def matrix(self): 1608 | """Return homogeneous rotation matrix.""" 1609 | return quaternion_matrix(self._qnow) 1610 | 1611 | 1612 | def arcball_map_to_sphere(point, center, radius): 1613 | """Return unit sphere coordinates from window coordinates.""" 1614 | v0 = (point[0] - center[0]) / radius 1615 | v1 = (center[1] - point[1]) / radius 1616 | n = v0*v0 + v1*v1 1617 | if n > 1.0: 1618 | # position outside of sphere 1619 | n = math.sqrt(n) 1620 | return numpy.array([v0/n, v1/n, 0.0]) 1621 | else: 1622 | return numpy.array([v0, v1, math.sqrt(1.0 - n)]) 1623 | 1624 | 1625 | def arcball_constrain_to_axis(point, axis): 1626 | """Return sphere point perpendicular to axis.""" 1627 | v = numpy.array(point, dtype=numpy.float64, copy=True) 1628 | a = numpy.array(axis, dtype=numpy.float64, copy=True) 1629 | v -= a * numpy.dot(a, v) # on plane 1630 | n = vector_norm(v) 1631 | if n > _EPS: 1632 | if v[2] < 0.0: 1633 | numpy.negative(v, v) 1634 | v /= n 1635 | return v 1636 | if a[2] == 1.0: 1637 | return numpy.array([1.0, 0.0, 0.0]) 1638 | return unit_vector([-a[1], a[0], 0.0]) 1639 | 1640 | 1641 | def arcball_nearest_axis(point, axes): 1642 | """Return axis, which arc is nearest to point.""" 1643 | point = numpy.array(point, dtype=numpy.float64, copy=False) 1644 | nearest = None 1645 | mx = -1.0 1646 | for axis in axes: 1647 | t = numpy.dot(arcball_constrain_to_axis(point, axis), point) 1648 | if t > mx: 1649 | nearest = axis 1650 | mx = t 1651 | return nearest 1652 | 1653 | 1654 | # epsilon for testing whether a number is close to zero 1655 | _EPS = numpy.finfo(float).eps * 4.0 1656 | 1657 | # axis sequences for Euler angles 1658 | _NEXT_AXIS = [1, 2, 0, 1] 1659 | 1660 | # map axes strings to/from tuples of inner axis, parity, repetition, frame 1661 | _AXES2TUPLE = { 1662 | 'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0), 1663 | 'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0), 1664 | 'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0), 1665 | 'szxz': (2, 0, 1, 0), 'szyx': (2, 1, 0, 0), 'szyz': (2, 1, 1, 0), 1666 | 'rzyx': (0, 0, 0, 1), 'rxyx': (0, 0, 1, 1), 'ryzx': (0, 1, 0, 1), 1667 | 'rxzx': (0, 1, 1, 1), 'rxzy': (1, 0, 0, 1), 'ryzy': (1, 0, 1, 1), 1668 | 'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1), 1669 | 'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)} 1670 | 1671 | _TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items()) 1672 | 1673 | 1674 | def vector_norm(data, axis=None, out=None): 1675 | """Return length, i.e. Euclidean norm, of ndarray along axis. 1676 | 1677 | >>> v = numpy.random.random(3) 1678 | >>> n = vector_norm(v) 1679 | >>> numpy.allclose(n, numpy.linalg.norm(v)) 1680 | True 1681 | >>> v = numpy.random.rand(6, 5, 3) 1682 | >>> n = vector_norm(v, axis=-1) 1683 | >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2))) 1684 | True 1685 | >>> n = vector_norm(v, axis=1) 1686 | >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1))) 1687 | True 1688 | >>> v = numpy.random.rand(5, 4, 3) 1689 | >>> n = numpy.empty((5, 3)) 1690 | >>> vector_norm(v, axis=1, out=n) 1691 | >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1))) 1692 | True 1693 | >>> vector_norm([]) 1694 | 0.0 1695 | >>> vector_norm([1]) 1696 | 1.0 1697 | 1698 | """ 1699 | data = numpy.array(data, dtype=numpy.float64, copy=True) 1700 | if out is None: 1701 | if data.ndim == 1: 1702 | return math.sqrt(numpy.dot(data, data)) 1703 | data *= data 1704 | out = numpy.atleast_1d(numpy.sum(data, axis=axis)) 1705 | numpy.sqrt(out, out) 1706 | return out 1707 | else: 1708 | data *= data 1709 | numpy.sum(data, axis=axis, out=out) 1710 | numpy.sqrt(out, out) 1711 | 1712 | 1713 | def unit_vector(data, axis=None, out=None): 1714 | """Return ndarray normalized by length, i.e. Euclidean norm, along axis. 1715 | 1716 | >>> v0 = numpy.random.random(3) 1717 | >>> v1 = unit_vector(v0) 1718 | >>> numpy.allclose(v1, v0 / numpy.linalg.norm(v0)) 1719 | True 1720 | >>> v0 = numpy.random.rand(5, 4, 3) 1721 | >>> v1 = unit_vector(v0, axis=-1) 1722 | >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=2)), 2) 1723 | >>> numpy.allclose(v1, v2) 1724 | True 1725 | >>> v1 = unit_vector(v0, axis=1) 1726 | >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=1)), 1) 1727 | >>> numpy.allclose(v1, v2) 1728 | True 1729 | >>> v1 = numpy.empty((5, 4, 3)) 1730 | >>> unit_vector(v0, axis=1, out=v1) 1731 | >>> numpy.allclose(v1, v2) 1732 | True 1733 | >>> list(unit_vector([])) 1734 | [] 1735 | >>> list(unit_vector([1])) 1736 | [1.0] 1737 | 1738 | """ 1739 | if out is None: 1740 | data = numpy.array(data, dtype=numpy.float64, copy=True) 1741 | if data.ndim == 1: 1742 | data /= math.sqrt(numpy.dot(data, data)) 1743 | return data 1744 | else: 1745 | if out is not data: 1746 | out[:] = numpy.array(data, copy=False) 1747 | data = out 1748 | length = numpy.atleast_1d(numpy.sum(data*data, axis)) 1749 | numpy.sqrt(length, length) 1750 | if axis is not None: 1751 | length = numpy.expand_dims(length, axis) 1752 | data /= length 1753 | if out is None: 1754 | return data 1755 | 1756 | 1757 | def random_vector(size): 1758 | """Return array of random doubles in the half-open interval [0.0, 1.0). 1759 | 1760 | >>> v = random_vector(10000) 1761 | >>> numpy.all(v >= 0) and numpy.all(v < 1) 1762 | True 1763 | >>> v0 = random_vector(10) 1764 | >>> v1 = random_vector(10) 1765 | >>> numpy.any(v0 == v1) 1766 | False 1767 | 1768 | """ 1769 | return numpy.random.random(size) 1770 | 1771 | 1772 | def vector_product(v0, v1, axis=0): 1773 | """Return vector perpendicular to vectors. 1774 | 1775 | >>> v = vector_product([2, 0, 0], [0, 3, 0]) 1776 | >>> numpy.allclose(v, [0, 0, 6]) 1777 | True 1778 | >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]] 1779 | >>> v1 = [[3], [0], [0]] 1780 | >>> v = vector_product(v0, v1) 1781 | >>> numpy.allclose(v, [[0, 0, 0, 0], [0, 0, 6, 6], [0, -6, 0, -6]]) 1782 | True 1783 | >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]] 1784 | >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]] 1785 | >>> v = vector_product(v0, v1, axis=1) 1786 | >>> numpy.allclose(v, [[0, 0, 6], [0, -6, 0], [6, 0, 0], [0, -6, 6]]) 1787 | True 1788 | 1789 | """ 1790 | return numpy.cross(v0, v1, axis=axis) 1791 | 1792 | 1793 | def angle_between_vectors(v0, v1, directed=True, axis=0): 1794 | """Return angle between vectors. 1795 | 1796 | If directed is False, the input vectors are interpreted as undirected axes, 1797 | i.e. the maximum angle is pi/2. 1798 | 1799 | >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3]) 1800 | >>> numpy.allclose(a, math.pi) 1801 | True 1802 | >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3], directed=False) 1803 | >>> numpy.allclose(a, 0) 1804 | True 1805 | >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]] 1806 | >>> v1 = [[3], [0], [0]] 1807 | >>> a = angle_between_vectors(v0, v1) 1808 | >>> numpy.allclose(a, [0, 1.5708, 1.5708, 0.95532]) 1809 | True 1810 | >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]] 1811 | >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]] 1812 | >>> a = angle_between_vectors(v0, v1, axis=1) 1813 | >>> numpy.allclose(a, [1.5708, 1.5708, 1.5708, 0.95532]) 1814 | True 1815 | 1816 | """ 1817 | v0 = numpy.array(v0, dtype=numpy.float64, copy=False) 1818 | v1 = numpy.array(v1, dtype=numpy.float64, copy=False) 1819 | dot = numpy.sum(v0 * v1, axis=axis) 1820 | dot /= vector_norm(v0, axis=axis) * vector_norm(v1, axis=axis) 1821 | return numpy.arccos(dot if directed else numpy.fabs(dot)) 1822 | 1823 | 1824 | def inverse_matrix(matrix): 1825 | """Return inverse of square transformation matrix. 1826 | 1827 | >>> M0 = random_rotation_matrix() 1828 | >>> M1 = inverse_matrix(M0.T) 1829 | >>> numpy.allclose(M1, numpy.linalg.inv(M0.T)) 1830 | True 1831 | >>> for size in range(1, 7): 1832 | ... M0 = numpy.random.rand(size, size) 1833 | ... M1 = inverse_matrix(M0) 1834 | ... if not numpy.allclose(M1, numpy.linalg.inv(M0)): print(size) 1835 | 1836 | """ 1837 | return numpy.linalg.inv(matrix) 1838 | 1839 | 1840 | def concatenate_matrices(*matrices): 1841 | """Return concatenation of series of transformation matrices. 1842 | 1843 | >>> M = numpy.random.rand(16).reshape((4, 4)) - 0.5 1844 | >>> numpy.allclose(M, concatenate_matrices(M)) 1845 | True 1846 | >>> numpy.allclose(numpy.dot(M, M.T), concatenate_matrices(M, M.T)) 1847 | True 1848 | 1849 | """ 1850 | M = numpy.identity(4) 1851 | for i in matrices: 1852 | M = numpy.dot(M, i) 1853 | return M 1854 | 1855 | 1856 | def is_same_transform(matrix0, matrix1): 1857 | """Return True if two matrices perform same transformation. 1858 | 1859 | >>> is_same_transform(numpy.identity(4), numpy.identity(4)) 1860 | True 1861 | >>> is_same_transform(numpy.identity(4), random_rotation_matrix()) 1862 | False 1863 | 1864 | """ 1865 | matrix0 = numpy.array(matrix0, dtype=numpy.float64, copy=True) 1866 | matrix0 /= matrix0[3, 3] 1867 | matrix1 = numpy.array(matrix1, dtype=numpy.float64, copy=True) 1868 | matrix1 /= matrix1[3, 3] 1869 | return numpy.allclose(matrix0, matrix1) 1870 | 1871 | 1872 | if __name__ == "__main__": 1873 | import doctest 1874 | import random # used in doctests 1875 | numpy.set_printoptions(suppress=True, precision=5) 1876 | doctest.testmod() 1877 | 1878 | 1879 | -------------------------------------------------------------------------------- /doc/HOD_file_format.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | 93 | 94 | 95 | 96 | 106 | 107 | HOD File Format (Regardless of type) 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 138 | 139 | 140 | 147 | 148 | 149 | 150 | 151 | 152 |
153 | 266 | 351 |
352 | 353 |
354 | 355 | 376 | 377 | 378 |
379 | 380 | 397 | 398 |
381 | 382 | 383 | 392 | 393 | 396 | 399 | 400 |
401 | 402 | 403 | 412 | 413 | 433 |
434 | 435 | 436 |
437 | 438 | 439 |
440 | 441 | 442 | 443 | 444 | 445 |
    446 | 454 |
455 |
456 | 457 | 458 | 459 | 460 |
461 | 462 | 463 |
464 | 465 |
466 | Results 1 to 3 of 3 467 |
468 |
469 |
470 |
471 |

HOD File Format (Regardless of type) 472 | 473 |

474 |
475 |
476 |
477 | 507 |
508 |
509 | 510 |
511 | 512 | 513 | 514 |
    515 | 516 |
  1. 517 | 518 | 519 |
    520 | 521 | 526 | 527 | 528 | 529 | 530 | 531 | #1 532 | 533 | 534 | 535 | 536 | 537 |
    538 |
    539 |
    540 |
    541 | 542 |
    543 | 4E534B 544 | 576 |
    577 | 4E534B is offline 578 | 579 | 580 |
    581 | 582 | Senior Member 583 | 584 | 585 | 586 | 587 | 588 | 589 |
    590 |
    591 |
    Join Date
    May 2006
    592 | 593 | 594 | 595 | 596 |
    597 | 598 | 599 | 602 | 603 |
    604 |
    605 |
    606 | 607 | 608 |

    609 | HOD File Format (Regardless of type) 610 |

    611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 |
    620 |
    621 |
    622 | So, this is complete-regardless of type file format for HODs ( and the one which CFHodEd uses, heyhey )
    623 |
    624 |
    625 |
    Code:
    626 |
    ##########################################################################################################################
     627 | ---- format ----
     628 | FORM
     629 | -> Version FORM
     630 | FORM
     631 | -> NAME FORM
     632 | FORM
     633 | -> HVMD FORM
     634 | -> -> LITE form
     635 | -> -> for each material, STAT FORM
     636 | -> -> for each textured material, LMIP FORM
     637 | -> -> VARY FORMS for each object
     638 | -> -> NRML FORM
     639 | -> -> -> BGLT FORM
     640 | -> -> FORM
     641 | -> -> -> BGMS FORM
     642 | -> -> -> -> Basic MeSHes form
     643 | -> -> SIMP FORM, for each SIMPle mesh
     644 | -> -> NRML FORM
     645 | -> -> -> MULTi-Mesh Form
     646 | -> -> -> -> "NRML" Form
     647 | -> -> -> -> -> Basic MeSH Form (LOD0)
     648 | -> -> -> -> -> -> For each material used, a polygonal object.
     649 | -> -> -> -> -> -> -> Face Strips (if needed)
     650 | -> -> -> -> -> -> -> Face List for misc faces (usually this is ALWAYS present)
     651 | -> -> -> -> "NRML" Form
     652 | -> -> -> -> -> Basic MeSH Form (LOD1)
     653 | -> -> -> -> "NRML" Form
     654 | -> -> -> -> -> Basic MeSH Form (LOD2)
     655 | -> -> -> -> "NRML" Form
     656 | -> -> -> -> -> Basic MeSH Form (LOD3)
     657 | -> -> -> -> "NRML" Form
     658 | -> -> -> -> -> Basic MeSH Form (LOD4)
     659 | -> -> -> -> "NRML" Form
     660 | -> -> -> -> -> Basic MeSH Form (LOD5)
     661 | -> -> -> -> "NRML" Form
     662 | -> -> -> -> -> Basic MeSH Form (LOD6)
     663 | -> -> -> -> "NRML" Form
     664 | -> -> -> -> -> Basic MeSH Form (LOD7)
     665 | -> -> -> -> "NRML" Form
     666 | -> -> -> -> -> Basic MeSH Form (LOD8)
     667 | -> -> -> -> "NRML" Form
     668 | -> -> -> -> -> Basic MeSH Form (LOD9)
     669 | -> -> "NRML" form
     670 | -> -> -> GOBlin-Group Form ( same as MULT, except that doesn't have "Number of LoDs" paramter in its Basic MeSH )
     671 | -> -> -> -> Basic MeSH Form
     672 | -> -> -> "GOBS" -> Out dated and not used
     673 | -> -> -> "GOBD" -> Out dated and unused
     674 | -> -> FORM
     675 | -> -> -> BackGround Star Group FORM [Only for Backgrounds]
     676 | FORM
     677 | -> DTRM Form
     678 | -> -> HEIRarchy Form
     679 | -> -> Engine Trail SHape Form
     680 | -> -> engine GLOW Form
     681 | -> -> engine BURN Form
     682 | -> -> NAVigation Lights Form
     683 | -> -> DAMaGe points Form -> (seems obsolete)
     684 | -> -> DOCKing paths Form
     685 | -> -> HARDpoints Form -> Not used now
     686 | -> -> MaRKeRs Form (may contain ANiMation forms)
     687 | -> -> STaR Fields form [Only for Backgrounds]
     688 | -> -> BouNDary Values Form
     689 | -> -> COLlision Detection meshes Form
     690 | -> -> Battle Scar Ref Mesh Form (NOT for Backgrounds)
     691 | -> -> -> "DESC" Form
     692 | -> -> -> Bounding BOX Form.
     693 | -> -> -> Boundary SPHere Form
     694 | -> -> -> "TRIS" Form (probably)
     695 | -> -> -> Vertex NoRMals Form
     696 | -> -> -> Spatial SUBdivision Form
     697 | FORM
     698 | -> INFO Form
     699 | [if needed]
     700 | -> -> WARN Form
     701 | -> -> -> Form Length
     702 | -> -> ERRR Form
     703 | -> -> -> Form Length
     704 | [usually always present]
     705 | -> -> OWNR Form
     706 | -> -> -> Form Length
     707 | ---- End ----
     708 | ##########################################################################################################################
     709 | ---- Version FORM ----
     710 | "FORM"
     711 | <length, in bytes, big endian> {ulong}
     712 | "VERS"
     713 | <version, always HEX:[00020000], ie 512, LittleEndian>
     714 | --- End ----
     715 | ---- NAME FORM ----
     716 | "FORM"
     717 | <length, in bytes, big endian> {ulong}
     718 | "NAME"
     719 | <type of file. Can be as follows:
     720 |  "Homeworld2 Multi Mesh File"
     721 |  "Homeworld2 Simple Mesh File"
     722 |  "Homeworld2 Wireframe Mesh File">
     723 | --- End ----
     724 | ---- HVMD FORM ----
     725 | (seems to be a non-deterministic section of data)
     726 | "FORM"
     727 | <length, in bytes, big endian> {ulong}
     728 | "HVMD"
     729 | LITE chunks for each light.
     730 | STAT chunks for each material.
     731 | LMIP chunks for materials.
     732 | MULT\GOBG chunks for objects.
     733 | ---- end ----
     734 | ---- LITE FORM ----
     735 | "LITE"
     736 | <length of this FORM, in bytes, big endian> {ulong}
     737 | <no of lights, little endian> {ulong}
     738 |  for each light
     739 |  --
     740 |  <length of light's name, in bytes, little endian> {ulong}
     741 |  <name of light> {char}
     742 |  <type of light: 0 = Ambient Light
     743 | 			   1 = Area Light
     744 | 			   2 = Directional Light
     745 | 			   3 = Point Light
     746 | 			   4 = Spot Light
     747 | 			   5 = Volume Light, little endian> {ulong}
     748 |  <X Y Z co-ords, LE> {float, ie single}
     749 |  <R G B Colour, LE> {float, ie single}
     750 |  <R G B Specularity, LE> {float, ie single}
     751 |  <Attenuation, LE> {ulong}
     752 |  <Attenuation Distance, LE> {float, ie single}
     753 |  --
     754 | ---- end ----
     755 | ---- STAT FORM ----
     756 | "STAT"
     757 | HEX:[000003E9] <- always next to stat, like an identifier. It's the version!!!
     758 | <length of material's name, in bytes, little endian> {ulong}
     759 | <name of the material> {char}
     760 | <length of shader's name, in bytes, little endian> {ulong}
     761 | <no of layers for this material, little endian> {ulong}
     762 |  TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT if zero, next chunk won't exist, end of STAT
     763 |  HEX: [0500000004000000]  <--- Looks like a identifer for a material with texture.
     764 |   TTTTTTTTTTTTTTTT   <--- Yes. This is an identifier for a textured material
     765 | <layer number, globally, for this 'parameter', little endian> {ulong}
     766 | <length of layer('parameter')'s name, little endian> {ulong}
     767 | <name of layer('parameter')> {char}
     768 | ---- end ----
     769 | ---- LMIP FORM ----
     770 | "LMIP"
     771 | <length of this LMIP chunk, in bytes, big endian> {ulong}
     772 | <length of file name with path, in bytes, little endian> {ulong}
     773 | <file name\path> {char}
     774 | <type of file> {char}
     775 | <no of mips, little endian> ulong
     776 | <width, little endian> ulong
     777 | <height, little endian> ulong
     778 | <data stream for picture>
     779 | ---- end ----
     780 | ---- VARY FORM ----
     781 | "VARY"
     782 | <length of this chunk, in bytes, big endian> {ulong}
     783 | <length of name of this mesh, Little Endian> {ulong}
     784 | <name of this mesh> {char}
     785 | <length of name of the joint, which this mesh is attached to, Little Endian> {ulong}
     786 | <name of the joint this mesh is attached to> {char}
     787 | <number of vertices, LE>, {ulong}
     788 | <number of wedges, LE>, {ulong}
     789 | <number of faces, LE>, {ulong}
     790 | <number of collapses (?), LE>, {ulong}
     791 | <number of uv sets, always 1, LE>, {ulong}
     792 | for each vertex
     793 |  <X Y Z position of the vert., LE>, {ulong}
     794 | for each wedge
     795 |  <vertex number, LE>, {ulong}
     796 |  <X Y Z position, LE>, {ulong}
     797 |  <U V co-ords., LE>, {ulong}
     798 |  <colour, LE>, {ulong}
     799 | for each face
     800 |  <material used, LE>, {ulong}
     801 |  <wedge used, LE>, {ulong}
     802 |  <face (?), LE>, {slong (!)}
     803 | for each collapse
     804 |  <number of fix-ups, LE>, {ulong}
     805 |  for each fix-up
     806 |   <type of fix, LE>, {ulong}
     807 |   <data0, LE>, {ulong}
     808 |   <data1, LE>, {ushort}
     809 |   <data2, LE>, {ushort}
     810 | ---- end ----
     811 | ---- BGLT FORM ----
     812 | "NRML"
     813 | <length of this chunk, in bytes, big endian> {ulong}
     814 | "BGLT"
     815 | <length of this chunk, in bytes, always ZERO???, big endian> {ulong}
     816 | ---- end ----
     817 | ---- BGMS FORM ----
     818 | "NRML"
     819 | <length of this chunk, in bytes, big endian> {ulong}
     820 | "BGMS"
     821 | <Basic MeSHes for different parts, usually 16>
     822 | ---- end ----
     823 | ---- SIMP FORM ----
     824 | "SIMP"
     825 | <length of this chunk, in bytes, big endian> {ulong}
     826 | <length of name of this mesh, this is zero for wire-frame(s) and single SIMP exists (!), Little Endian> {ulong}
     827 | <name of this mesh> {char}
     828 | <number of vertices, LE>, {ulong}
     829 | <vertex data...>
     830 | <num face lists\strips, this is zero in wire-frame(s) LE>, {ulong}
     831 | <face list data..., note: this is LONGs instead of SHORTs in the usual format (!)>
     832 | ---- end ----
     833 | ---- MULT FORM ----
     834 | (mult = multi mesh, for ships, somewhat like simple meshes)
     835 | "NRML"
     836 | <form length, BE> {ulong}
     837 | "MULT" or "GOBG"
     838 | <version, of this MultiMesh 
     839 |  Note: This should ALWAYS be 1400 (for MULT) or 1000 (for GOBG), big endian(BE)> {ulong}
     840 | <length of name of this mesh, Little Endian> {ulong}
     841 | <name of this mesh> {char}
     842 | <length of name of the joint, which this mesh is attached to, Little Endian> {ulong}
     843 | <name of the joint this mesh is attached to> {char}
     844 | <number of BMSH forms in this MM form, LE, not present for "GOBG" chunks> {ulong}
     845 |   BMSH forms, for each lod...
     846 | ---- end -----
     847 | ---- BMSH FORM ----
     848 | "NRML"
     849 | <num bytes for this form>, {ulong}
     850 | "BMSH"
     851 | <version, of this BasicMesh.
     852 |  Note: This should ALWAYS be 1400 (type doesn't matter), big endian(BE)> {ulong}
     853 | <lod of the mesh(es), Little Endian(LE)> {ulong}
     854 | <number of mesh(es) in this BMSH form, LE> {ulong}
     855 | <STAT(material) number used, probably LE> {ulong}
     856 | <vertex mask, this is 27 for Normal MULTi-meshes, and 11 if the vertex data only has X Y Z co-ords (no normals, UVs, of Colours) (ie not a background), LE > {ulong} ???
     857 | <num vertices in this part, LE> {ulong}
     858 | <vertex data...>
     859 | <num face lists\strips, LE>, {ulong}
     860 | <face list data...>
     861 | ---- vtxd ----
     862 | <co-ords, LE> {float, ie single}
     863 | <filler1, always 1, LE> {float, ie single}
     864 | <vertex normal, LE> {float, ie single}
     865 | <filler2, always 1, LE> {float, ie single}
     866 | <uv coords> {float, ie single}
     867 | ---- end ----
     868 | ---- facd ----
     869 | <type of facelists, 514( (optimized?) strips) OR 518(lists), LE>, {ulong}
     870 | <number of faces this this list, not faces, but entries,
     871 |   for lists, this is numFaces * 3, for strips, this is, should be, numFaces - 2, LE>, {ulong}
     872 | <face list, in triplets, LE>, ushort (Important!!! This starts from zero! wOBJ starts from 1!)
     873 |   (Strips are a bit... different that lists, Stay Sharp)
     874 | ---- end ---- 
     875 | ---- HEIR FORM ----
     876 | "HEIR"
     877 | <num bytes for this form>, {ulong}
     878 | <number of joints, LE>, {ulong}
     879 | - for each joint -
     880 |   <length of name of joint, LE>, {ulong}
     881 |   <name of joint>, {char}
     882 |   <length of name of parent, LE>, {ulong}
     883 |   <name of parent>, {char} <- note, this CAN be null, and above one zero...
     884 |   <position in X,Y,Z axis, LE>, float
     885 |   <rotation in X,Y,Z axis, this is radians, LE>, float
     886 |   <scale in X,Y,Z axis, LE>, float
     887 |   <axis of this joint, ie 0,0,0 is normal, 0,180,0 means reverse Z-axis, 
     888 |   ie maybe this is applied to it's children after rotation. again, this is 
     889 |   in radians, not degrees, LE>, float
     890 |   <freedom, in X, Y, Z axis, this is zero for 'EngineNozzle's
     891 | 	note: in Maya, children joints\objects can be rotated if deg. of freedom is 
     892 | 	off. This may not be true in game, Also, it is POSSIBLE to give rotation 
     893 | 	even when deg. of freedom is false, or zero... LE>, {bool}
     894 | - next joint -
     895 | ---- end ----
     896 | ---- ETSH FORM ----
     897 | "ETSH"
     898 | <num bytes for this form>, {ulong}
     899 | <length of name of this mesh, Little Endian> {ulong}
     900 | <name of this mesh> {char}
     901 | <length of name of the joint, which this mesh is attached to, Little Endian> {ulong}
     902 | <name of the joint this mesh is attached to> {char}
     903 | <number of vertices, LE>, {ulong}
     904 | <vertices, (these are usually in form of QUADs) position at X Y Z axis ONLY, LE>, {float}
     905 | ---- end ----
     906 | ---- GLOW FORM ----
     907 | "FORM"
     908 | <num bytes for this form>, {ulong}
     909 | "GLOW"
     910 | <"INFO":
     911 |   <version, always 1000, BE>, unlong
     912 |   <length of name of this mesh, LE> {ulong}
     913 |   <name of this mesh> {char}
     914 |   <length of name of the joint, which this mesh is attached to, LE> {ulong}
     915 |   <name of the joint this mesh is attached to>, {char}
     916 |   <LoD of this mesh, LE>, {ulong} >
     917 | <BMSH Forms for LoD>
     918 | ---- end ----
     919 | ---- BURN FORM ----
     920 | "BURN"
     921 | <num bytes for this form>, {ulong}
     922 | <length of name of this mesh, Little Endian> {ulong}
     923 | <name of this mesh> {char}
     924 | <length of name of the joint, which this mesh is attached to, Little Endian> {ulong}
     925 | <name of the joint this mesh is attached to> {char}
     926 | <number of vertices, LE>, {ulong}
     927 | <nurbs count, this should be one, LE>, {ulong}
     928 | <vertices, position at X Y Z axis ONLY, LE>, {float}
     929 |   note: these are usually a NURBS Curve, but are points, 
     930 |   later points have higher Z position, X,Y deviation is possible
     931 | ---- end ----
     932 | ---- NAVL FORM ----
     933 | "NRML"
     934 | <num bytes for this form>, {ulong}
     935 | "NAVL"
     936 | <version, LE>, {ulong}
     937 | <number of NavLights, LE>, {ulong}
     938 | -- for each NavLight --
     939 |   <length of name of this navlight, LE>, {ulong}
     940 |   <name of navlight>, {char}
     941 |   <section, LE>, {ulong}
     942 |   <size, LE>, {float}
     943 |   <phase, LE>, {float}
     944 |   <frequency, LE>, {float}
     945 |   <length of style name> {ulong}
     946 |   <style> {char}
     947 |   <colour, RGBA, normalized, LE>, {float}
     948 |   <distance of light casting, LE>, {float}
     949 |   <sprite is visible>, {bool}
     950 |   <light casted on high-end systems only>, {bool}
     951 | -- next Navlight --
     952 | ---- end ----
     953 | ---- DOCK FORM ----
     954 | "DOCK"
     955 | <num bytes for this form>, {ulong}
     956 | <num of paths, LE>, {ulong}
     957 | -- for each path --
     958 |   <length of name of this path, LE>, {ulong}
     959 |   <name of path>, {char}
     960 |   <length of name of parent of this path, LE>, {ulong}
     961 |   <name of parent, >, {char}
     962 | 	note: that unlike joints, this is world, instead of null, and usually this IS
     963 | 	world, instead of joint>, {char}
     964 |   <number of properties, global for this path, this is SIX, LE>, {ulong}
     965 |   <number of properties, for point on the path, this is TEN, LE>, {ulong}
     966 |   <is this a exit path?, LE>, {ulong}
     967 |   <is this a latch path?, LE>, {ulong}
     968 |   <global tolerence, this is obsolete and should ALWAYS be zero, LE>, {float}
     969 |   <length of name of dockable families, LE>, {ulong}
     970 |   <dockable families>, {char}
     971 |   <use animations?, LE>, {ulong}
     972 |   <length of name of linked paths, LE>, {ulong}
     973 |   <linked paths>, {char}
     974 |   <number of frames, LE>, {ulong}
     975 |   -- for each frame --
     976 | 	<X Y Z position at this frame, LE>, {float}
     977 | 	<X Y Z orientation at this frame, again, this is in radians, LE>, {float}
     978 | 	<use rotation?, LE>, {ulong}
     979 | 	<point tolerance, LE>, {float}
     980 | 	<drop focus?, LE>, {ulong}
     981 | 	<max speed, LE>, {float}
     982 | 	<check rotation before proceeding?, LE>, {ulong}
     983 | 	<force close behaviour, LE>, {ulong}
     984 | 	<player is in control?, LE>, {ulong}
     985 | 	<queue origin?, LE>, {ulong}
     986 | 	<use clip plane?, LE>, {ulong}
     987 | 	<clear reservation?, LE>, {ulong}
     988 |   -- next frame --
     989 | -- next path --
     990 | ---- end ----
     991 | ---- MRKR FORM ----
     992 | "FORM"
     993 | <num bytes for this form>, {ulong}
     994 | "MRKR" 
     995 | "NRML"
     996 | <num bytes for this sub-form>
     997 | -- -- -- --
     998 |   "HEAD"
     999 |   <version, for this sub sub-form, this is ONE for MARKERS, BE>, {ulong}
    1000 |   <length of name, LE>, {ulong}
    1001 |   <name>, {char}
    1002 |   <length of name of parent, LE>, {ulong}
    1003 |   <name of parent>, {char}
    1004 |   <start keyframe time, this is 24fps, LE>, {float}
    1005 |   <end keyframe time, this is 24fps, LE>, {float}
    1006 |   <X Y Z position, LE>, {double}
    1007 |   <X Y Z rotation, this is in radians, LE>, {double}
    1008 | -- -- -- --
    1009 |   "KEYF"
    1010 |   <num bytes for this sub-form>
    1011 | 	"ANIM"
    1012 | 	<version, for this sub sub-form, this is ONE here, LE>, {ulong}
    1013 | 	<length of name of curve, LE>, {ulong}
    1014 | 	<name of curve>, {char}
    1015 | 	<number of keyframes, LE>, {ulong}
    1016 | 	-- for each keyframe --
    1017 | 	  <time, LE>, {double}
    1018 | 	  <Value, LE>, {double}
    1019 | 	  <inTangentX, LE>, {float}
    1020 | 	  <inTangentY, LE>, {float}
    1021 | 	  <outTangentX, LE>, {float}
    1022 | 	  <outTangentY, LE>, {float}
    1023 | 	-- next keyframe --
    1024 | 	<Pre-infinity, LE>, {ulong}
    1025 | 	<Post-infinity, LE>, {ulong}
    1026 | ---- end ----
    1027 | --- BGSG FORM ----
    1028 | "FORM"
    1029 | <length of this chunk, in bytes, big endian> {ulong}
    1030 | "BGSG"
    1031 | <length of name of texture used, for stars, here, LE>, {ulong}
    1032 | <name of texture used, for stars, here>, {char}
    1033 | <number of star objects, LE>, {ulong}
    1034 |   -- -- -- --[for each star]-- -- -- --
    1035 |   <X Y Z position of the star, LE>, {float}
    1036 |   <size of the star, LE>, {float}
    1037 |   <R G B A colour of the star, LE>, {float}
    1038 |   -- -- -- -- -- -- -- -- -- -- -- --
    1039 | ---- end ----
    1040 | ---- STRF FORM ----
    1041 | "NRML"
    1042 | <num bytes for this form, BE>, {ulong}
    1043 | "STRF"
    1044 | <version, for this sub sub-form, this is 1000 for STRF, BE>, {ulong}
    1045 | <number of star objects, LE>, {ulong}
    1046 |   -- -- -- --[for each star]-- -- -- --
    1047 |   <X Y Z position of the star, LE>, {float}
    1048 |   <R G B colour of the star, LE>, {float}
    1049 |   <size of the star, LE>, {float}
    1050 |   -- -- -- -- -- -- -- -- -- -- -- --
    1051 | ---- end ----
    1052 | ---- BNDV FORM ----
    1053 | "BNDV"
    1054 | <num bytes for this form>, {ulong}
    1055 | <length of name of this object, LE>, {ulong}
    1056 | <name of this object>, {char}
    1057 | <length of name of parent of this object, LE>, {ulong}
    1058 | <name of object, always ROOT???>, {ulong}
    1059 | <number of vetices, or elements, this is ALWAYS 8, LE>, {ulong}
    1060 | <vertices, with X Y Z co-ords only.
    1061 |  Note: only the 1st and 7th vertex have data.
    1062 |  Rest ALL are ZERO ( 2-6 and 8 ), LE>, {float}
    1063 | <side information, ALWAYS 0, LE>, {ulong}
    1064 | ---- end ----
    1065 | ---- COLD FORM ----
    1066 | "FORM"
    1067 | <num bytes for this form>, {ulong}
    1068 | <length of name of joint for which this CM info is intended, LE>, {ulong}
    1069 | <name of joint for which this CM info is intended, LE>, {ulong}
    1070 |   -- -- -- --
    1071 |   "BBOX"
    1072 |   <length of this Form, BE>, {ulong}
    1073 |   <left-top-back extens, X Y Z, LE>, {float}
    1074 |   <right-bottom-front extents, X Y Z, LE>, {float}
    1075 |   -- -- -- --
    1076 |   "BSPH"
    1077 |   <version, this is 1000, BE>, {ulong}
    1078 |   <X Y Z center of this sphere, LE>, {float}
    1079 |   <radius of this sphere, LE>, {float}
    1080 |   -- -- -- --
    1081 |   "TRIS"			   -> This seems to be the collision Mesh itself
    1082 |   <version, this is 1000, again and again... BE>, {ulong}
    1083 |   <num vertices, LE>, {ulong}
    1084 |   <vertex data, for each vertex only X Y Z, no UVs or Normals, LE>, {float}
    1085 |   <number of entries, ie num faces * 3>  
    1086 |   <face data, in triplets, LE>, {ulong}
    1087 | ---- end ----
    1088 | ---- BSRM FORM ----
    1089 | "FORM"
    1090 | <num bytes for this form>, {ulong}
    1091 | "BSRM"
    1092 | "NRML"
    1093 | <num bytes for this sub-form>, {ulong}
    1094 |   "DESC"
    1095 |   <version, this is 1000, BE>, {ulong}
    1096 |   <length of name of this object's parent mesh, Little Endian> {ulong}
    1097 |   <name of this object's parent mesh> {char}
    1098 |   <length of name of the joint, which this object's parent mesh is attached to, LE> {ulong}
    1099 |   <name of the joint this object's parent mesh is attached to> {char}
    1100 |   <LoD of this object's parent mesh, LE>, {ulong}
    1101 | "NRML"
    1102 | <num bytes for this sub-form>, {ulong}
    1103 |   "BBOX"
    1104 |   <version, this is 1000, BE>, {ulong}
    1105 |   <left-top-back extens, X Y Z, LE>, {float}
    1106 |   <right-bottom-front extents, X Y Z, LE>, {float}
    1107 | "NRML"
    1108 | <num bytes for this sub-form>, {ulong}
    1109 |   "BSPH"
    1110 |   <version, this is 1000, BE>, {ulong}
    1111 |   <X Y Z center of this sphere, LE>, {float}
    1112 |   <radius of this sphere, LE>, {float}
    1113 | "NRML"
    1114 | <num bytes for this sub-form>, {ulong}
    1115 |   "TRIS"
    1116 |   <version, this is 1000, again and again... BE>, {ulong}
    1117 |   <num vertices, LE>, {ulong}
    1118 |   <vertex data, for each vertex only X Y Z, no UVs or Normals, LE>, {float}
    1119 |   <number of entries, ie num faces * 3>
    1120 |   <face data, in triplets, LE>, {ulong}
    1121 | "NRML"
    1122 | <num bytes for this sub-form>, {ulong}
    1123 |   "VNRM"		   -> And this looks like normals for TRIS, like numVerts there is equal to numNormals here.
    1124 |   <version, this is, *sigh*, 1000, BE>, {ulong}
    1125 |   <number of normals present, LE>, {ulong}
    1126 |   <vertex normal data, for each vertex only X Y Z Normals, LE>, {float}
    1127 |   <number of face-Indices, LE>, {ulong}
    1128 |   <face indice data, in triplets, LE>, {ulong}
    1129 | "NRML"
    1130 | <num bytes for this sub-form>, {ulong}
    1131 |   "SSUB"			 -> This is calculated by basis of "grids" in the plugins.
    1132 |   <version, this is 1000, BE>, {ulong}
    1133 |   <num Sub-Divisions in X Plane, LE>, {ulong}
    1134 |   <num Sub-Divisions in Y Plane, LE>, {ulong}
    1135 |   <num Sub-Divisions in Z Plane, LE>, {ulong}
    1136 |   -- for each sub division [ grid (!) ], ie X*Y*Z --
    1137 | 	<number of triangles, LE>, {ulong}
    1138 | 	<face list, which are to be sub-divided>
    1139 | ---- end ----
    1140 | ---- INFO FORM ----
    1141 | "FORM"
    1142 | <num bytes for this form>, {ulong}
    1143 | "INFO"
    1144 | [if needed, these two maybe present]
    1145 | "WARN"
    1146 | <length of warning + 1, LE>, {ulong}
    1147 | <warning + NULL>, {char}				<- looks like that NULL is string terminator here 
    1148 | "ERRR"
    1149 | <length of error + 1, LE>, {ulong}
    1150 | <error + NULL>, {char}				 <- looks like that NULL is string terminator here 
    1151 | [usually this is always present]
    1152 | "OWNR"
    1153 | <length of name of Owner + 1, LE>, {ulong}
    1154 | <Name of Owner + NULL>, {char}		 <- looks like that NULL is string terminator here 
    1155 | ---- end ----
    1156 | ##########################################################################################################################
    1157 | ##########################################################################################################################
    1158 |
    Sounds informative? I've taken this information mostly from the RDN plugins. Some parts (for MULTs\GOBGs are from 'HW2 HOD Reporter' by Pesmontis) 1159 |
    1160 |
    1161 | 1162 | 1163 |
    1164 |
    1165 | 1166 |
    1167 | 1168 | 1169 |
    1170 | 1171 | 1172 | Last edited by 4E534B; 9th Sep 06 at 7:50 AM. 1173 | 1174 | 1175 |
    1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 |
    1183 | 1184 |
    1185 |
    1186 |
    1187 |
    1188 | 1189 |
    1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1214 | 1215 |
    1216 |
    1217 |
    1218 |
  2. 1219 | 1220 |
  3. 1221 | 1222 | 1223 |
    1224 | 1225 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | #2 1236 | 1237 | 1238 | 1239 | 1240 | 1241 |
    1242 |
    1243 |
    1244 |
    1245 | 1246 |
    1247 | 4E534B 1248 | 1280 |
    1281 | 4E534B is offline 1282 | 1283 | 1284 |
    1285 | 1286 | Senior Member 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 |
    1294 |
    1295 |
    Join Date
    May 2006
    1296 | 1297 | 1298 | 1299 | 1300 |
    1301 | 1302 | 1303 | 1306 | 1307 |
    1308 |
    1309 |
    1310 | 1311 | 1312 |

    1313 | VB File Format for Multi-Mesh HOD 1314 |

    1315 | 1316 | 1317 | 1318 | 1319 | 1320 |
    1321 |
    1322 |
    1323 | *BIIIIG BUMP!!!*
    1324 |
    1325 |
    1326 |
    Code:
    1327 |
    Public Type HW2_VersionForm
    1328 | ID As String * 4
    1329 | formLength As Long
    1330 | versionID As String * 4
    1331 | version As Long
    1332 | End Type
    1333 | Public Type HW2_NAMEForm
    1334 | ID As String * 4
    1335 | formLength As Long
    1336 | nameID As String * 4
    1337 | type As String
    1338 | End Type
    1339 | Public Type HW2_STATForm_Parameter
    1340 | ID As String * 8
    1341 | layerNum As Long
    1342 | lenParameter As Long
    1343 | parameter As String
    1344 | End Type
    1345 | Public Type HW2_STATForm
    1346 | formID As String * 4
    1347 | formLength As Long
    1348 | ID As String * 4
    1349 | version As String * 4
    1350 | matNameLength As Long
    1351 | matName As String
    1352 | shaderNameLength As Long
    1353 | shaderName As String
    1354 | numLayers As Long
    1355 | parameters() As HW2_STATForm_Parameter
    1356 | End Type
    1357 | Public Type HW2_DDSFile
    1358 | imageStream() As Double
    1359 | End Type
    1360 | Public Type HW2_LMIPForm
    1361 | ID As String * 4
    1362 | formLength As Long
    1363 | fileNameLength As Long
    1364 | fileName As String
    1365 | fileType As String * 4
    1366 | noMips As Long
    1367 | width As Long
    1368 | height As Long
    1369 | data() As HW2_DDSFile
    1370 | End Type
    1371 | Public Type HW2_MULT_Vertex
    1372 | translateX As Single
    1373 | translateY As Single
    1374 | translateZ As Single
    1375 | filler1 As Single
    1376 | vertNormalX As Single
    1377 | vertNormalY As Single
    1378 | vertNormalZ As Single
    1379 | filler2 As Single
    1380 | textureU As Single
    1381 | textureV As Single
    1382 | End Type
    1383 | Public Type HW2_MULT_Face
    1384 | typeOfFaceList As Long
    1385 | numFacesInThisList As Long
    1386 | vertex() As Integer
    1387 | End Type
    1388 | Public Type HW2_MULT_FaceData
    1389 | faceList() As HW2_MULT_Face
    1390 | End Type
    1391 | Public Type HW2_BMSHForm_Group
    1392 | materialIndex As Long
    1393 | dwordPerVertex As Long
    1394 | numVertices As Long
    1395 | vertData() As HW2_MULT_Vertex
    1396 | numFaceListsOrStrips As Integer
    1397 | faceData As HW2_MULT_FaceData
    1398 | End Type
    1399 | Public Type HW2_BMSHForm
    1400 | ID As String * 4
    1401 | formLength As Long
    1402 | ID2 As String * 4
    1403 | version As Long
    1404 | meshLOD As Long
    1405 | numMeshes As Long
    1406 | mesh() As HW2_BMSHForm_Group
    1407 | End Type
    1408 | Public Type HW2_MULTForm
    1409 | ID As String
    1410 | formLength As Long
    1411 | ID2 As String * 4
    1412 | version As Long
    1413 | nameLength As Long
    1414 | name As String
    1415 | parentNameLength As Long
    1416 | parentName As String
    1417 | numBMSHForms As Long
    1418 | BMSHForms() As HW2_BMSHForm
    1419 | End Type
    1420 | Public Type HW2_HVMDForm
    1421 | ID As String * 4
    1422 | formLength As Long
    1423 | ID2 As String * 4
    1424 | STATForms() As HW2_STATForm
    1425 | LMIPForms() As HW2_LMIPForm
    1426 | MULTForms() As HW2_MULTForm
    1427 | End Type
    1428 | Public Type HW2_HIERJoint
    1429 | lenName As Long
    1430 | name As String
    1431 |  
    1432 | lenParentName As Long
    1433 | parentName As String
    1434 |  
    1435 | translateX As Single
    1436 | translateY As Single
    1437 | translateZ As Single
    1438 |  
    1439 | rotateX As Single
    1440 | rotateY As Single
    1441 | rotateZ As Single
    1442 |  
    1443 | scaleX As Single
    1444 | scaleY As Single
    1445 | scaleZ As Single
    1446 |  
    1447 | axisX As Single
    1448 | axisY As Single
    1449 | axisZ As Single
    1450 |  
    1451 | degOfFreedomX As Byte
    1452 | degOfFreedomY As Byte
    1453 | degOfFreedomZ As Byte
    1454 | End Type
    1455 | Public Type HW2_HIERForm
    1456 | ID As String * 4
    1457 | formLength As Long
    1458 | numJoints As Long
    1459 | joints() As HW2_HIERJoint
    1460 | End Type
    1461 | Public Type HW2_3DVector
    1462 | X As Single
    1463 | Y As Single
    1464 | Z As Single
    1465 | End Type
    1466 | Public Type HW2_ETSHForm
    1467 | ID As String * 4
    1468 | formLength As Long
    1469 |  
    1470 | lenName As Long
    1471 | name As String
    1472 |  
    1473 | lenParentName As Long
    1474 | parentName As String
    1475 |  
    1476 | numVerts As Long
    1477 | vertData() As HW2_3DVector
    1478 | End Type
    1479 | Public Type HW2_GLOWForm
    1480 | ID As String * 4
    1481 | formLength As Long
    1482 | glowID As String * 4
    1483 |  
    1484 | nrmlID As String * 4
    1485 | nrmlFormLength As Long
    1486 |  
    1487 | infoID As String * 4
    1488 | version As Long
    1489 |  
    1490 | lenName As Long
    1491 | name As String
    1492 |  
    1493 | lenParentName As Long
    1494 | parentName As String
    1495 |  
    1496 | meshLOD As Long
    1497 | mesh As HW2_BMSHForm
    1498 | End Type
    1499 | Public Type HW2_BURNForm
    1500 | ID As String * 4
    1501 | formLength As Long
    1502 |  
    1503 | lenName As Long
    1504 | name As String
    1505 |  
    1506 | lenParentName As Long
    1507 | parentName As String
    1508 |  
    1509 | numVerts As Long
    1510 | numNURBS As Long
    1511 |  
    1512 | vertData() As HW2_3DVector
    1513 | End Type
    1514 | Public Type HW2_NavLight
    1515 | lenName As Long
    1516 | name As String
    1517 |  
    1518 | section As Long
    1519 | size As Single
    1520 | phase As Single
    1521 | frequency As Single
    1522 |  
    1523 | lenStyle As Long
    1524 | style As String
    1525 |  
    1526 | colourR As Single
    1527 | colourG As Single
    1528 | colourB As Single
    1529 | colourA As Single
    1530 |  
    1531 | distance As Single
    1532 | isVisible As Byte
    1533 | castLightOnHES As Byte
    1534 | End Type
    1535 | Public Type HW2_NAVLForm
    1536 | ID As String * 4
    1537 | formLength As Long
    1538 | navlID As String * 4
    1539 | version As Long
    1540 | numNavlights As Long
    1541 | navlights() As HW2_NavLight
    1542 | End Type
    1543 | Public Type HW2_DockPathFrame
    1544 | translateX As Single
    1545 | translateY As Single
    1546 | translateZ As Single
    1547 |  
    1548 | rotateX As Single
    1549 | rotateY As Single
    1550 | rotateZ As Single
    1551 |  
    1552 | useRotation As Long
    1553 | pointTolerance As Single
    1554 | dropFocus As Long
    1555 | maxSpeed As Single
    1556 | checkRotation As Long
    1557 | forceCloseBehavior As Long
    1558 | playerIsInControl As Long
    1559 | queueOrigin As Long
    1560 | useClipPlane As Long
    1561 | clearReservation As Long
    1562 | End Type
    1563 | Public Type HW2_DockPath
    1564 | lenName As Long
    1565 | name As String
    1566 |  
    1567 | lenParentName As Long
    1568 | parentName As String
    1569 |  
    1570 | numGlobalProperties As Long
    1571 | numLocalProperties As Long
    1572 |  
    1573 | isExit As Long
    1574 | isLatch As Long
    1575 | tolerance As Single
    1576 |  
    1577 | lenDockFamilies As Long
    1578 | dockFamilies As String
    1579 |  
    1580 | useAnim As Long
    1581 |  
    1582 | lenLinkedPaths As Long
    1583 | linkedPaths As String
    1584 |  
    1585 | numFrames As Long
    1586 | frames() As HW2_DockPathFrame
    1587 | End Type
    1588 | Public Type HW2_DOCKForm
    1589 | ID As String * 4
    1590 | formLength As Long
    1591 | numPaths As Long
    1592 | dockPaths() As HW2_DockPath
    1593 | End Type
    1594 | Public Type HW2_MRKRKeyFrame
    1595 | time As Double
    1596 | value As Double
    1597 |  
    1598 | inTangentX As Single
    1599 | inTangentY As Single
    1600 | outTangentX As Single
    1601 | outTangentY As Single
    1602 | End Type
    1603 | Public Type HW2_ANIMForm
    1604 | nrmlID_KEYF As String * 4
    1605 | formLength_NRML_KEYF As Long
    1606 |  
    1607 | animID As String * 4
    1608 | animVersion As Long
    1609 |  
    1610 | lenCurveName As Long
    1611 | curveName As String
    1612 |  
    1613 | numKeyFrames As Long
    1614 |  
    1615 | keyFrames() As HW2_MRKRKeyFrame
    1616 |  
    1617 | preInfinity As Long
    1618 | postInfinity As Long
    1619 | End Type
    1620 | Public Type HW2_MRKRForm
    1621 | ID As String * 4
    1622 | formLength As Long
    1623 | mrkrID As String * 4
    1624 |  
    1625 | nrmlID As String * 4
    1626 | formLength_nrml As Long
    1627 |  
    1628 | headID As String * 4
    1629 | version As Long
    1630 |  
    1631 | lenName As Long
    1632 | name As String
    1633 |  
    1634 | lenParentName As Long
    1635 | parentName As String
    1636 |  
    1637 | startKeyFrameTime As Single
    1638 | endKeyFrameTime As Single
    1639 |  
    1640 | translateX As Double
    1641 | translateY As Double
    1642 | translateZ As Double
    1643 |  
    1644 | rotateX As Double
    1645 | rotateY As Double
    1646 | rotateZ As Double
    1647 |  
    1648 | formID As String * 4
    1649 | formLength_KEYF As Long
    1650 | keyfID As String * 4
    1651 | anim() As HW2_ANIMForm
    1652 | End Type
    1653 | Public Type HW2_BNDVForm
    1654 | ID As String * 4
    1655 | formLength As Long
    1656 |  
    1657 | lenName As Long
    1658 | name As String
    1659 |  
    1660 | lenParentName As Long
    1661 | parentName As String
    1662 |  
    1663 | numVertices As Long
    1664 | vertData() As HW2_3DVector
    1665 | sideInformation As Long
    1666 | End Type
    1667 | Public Type HW2_BBOXForm
    1668 | ID As String * 4
    1669 | versionOrFormLength As Long
    1670 |  
    1671 | minX As Single
    1672 | minY As Single
    1673 | minZ As Single
    1674 |  
    1675 | maxX As Single
    1676 | maxY As Single
    1677 | maxZ As Single
    1678 | End Type
    1679 | Public Type HW2_BSPHForm
    1680 | ID As String * 4
    1681 | versionOrFormLength As Long
    1682 |  
    1683 | centerX As Single
    1684 | centerY As Single
    1685 | centerZ As Single
    1686 |  
    1687 | radius As Single
    1688 | End Type
    1689 | Public Type HW2_Face_List
    1690 | vertex() As Long
    1691 | End Type
    1692 | Public Type HW2_TRISForm
    1693 | ID As String * 4
    1694 | versionOrFormLength As Long
    1695 | numVerts As Long
    1696 | vertData() As HW2_3DVector
    1697 |  
    1698 | numFaces As Long
    1699 | faceData() As Integer
    1700 | End Type
    1701 | Public Type HW2_COLDForm
    1702 | ID As String * 4
    1703 | formLength As Long
    1704 | ID2 As String * 4
    1705 |  
    1706 | lenParentName As Long
    1707 | parentName As String
    1708 |  
    1709 | bbox As HW2_BBOXForm
    1710 | bsph As HW2_BSPHForm
    1711 | tris As HW2_TRISForm
    1712 | End Type
    1713 | Public Type HW2_DESCForm
    1714 | ID As String * 4
    1715 | version As Long
    1716 |  
    1717 | lenName As Long
    1718 | name As String
    1719 |  
    1720 | lenParentName As Long
    1721 | parentName As String
    1722 |  
    1723 | lod As Long
    1724 | End Type
    1725 | Public Type HW2_VNRMForm
    1726 | ID As String * 4
    1727 | version As Long
    1728 | numNormals As Long
    1729 | normalData() As HW2_3DVector
    1730 | numFaceIndices As Long
    1731 | faceData() As Integer
    1732 | End Type
    1733 | Public Type HW2_SSUB_Division
    1734 | numTriangles As Long
    1735 | faces() As Integer
    1736 | End Type
    1737 | Public Type HW2_SSUBForm
    1738 | ID As String * 4
    1739 | version As Long
    1740 |  
    1741 | numDivsX As Long
    1742 | numDivsY As Long
    1743 | numDivsZ As Long
    1744 |  
    1745 | subD() As HW2_SSUB_Division
    1746 | End Type
    1747 | Public Type HW2_BSRMForm
    1748 | ID As String * 4
    1749 | formLength As Long
    1750 | bsrmID As String * 4
    1751 |  
    1752 | nrmlDESCID As String * 4
    1753 | nrmlDESCFormLength As Long
    1754 | desc As HW2_DESCForm
    1755 |  
    1756 | nrmlBBOXID As String * 4
    1757 | nrmlBBOXFormLength As Long
    1758 | bbox As HW2_BBOXForm
    1759 |  
    1760 | nrmlBSPHID As String * 4
    1761 | nrmlBSPHFormLength As Long
    1762 | bsph As HW2_BSPHForm
    1763 |  
    1764 | nrmlTRISID As String * 4
    1765 | nrmlTRISFormLength As Long
    1766 | tris As HW2_TRISForm
    1767 |  
    1768 | nrmlVNRMID As String * 4
    1769 | nrmlVNRMFormLength As Long
    1770 | vnrm As HW2_VNRMForm
    1771 |  
    1772 | nrmlSSUBID As String * 4
    1773 | nrmlSSUBFormLength As Long
    1774 | ssub As HW2_SSUBForm
    1775 | End Type
    1776 | Public Type HW2_DTRMForm
    1777 | ID As String * 4
    1778 | formLength As Long
    1779 | ID2 As String * 4
    1780 |  
    1781 | hier As HW2_HIERForm
    1782 |  
    1783 | etsh() As HW2_ETSHForm
    1784 | glow() As HW2_GLOWForm
    1785 | burn() As HW2_BURNForm
    1786 |  
    1787 | navl As HW2_NAVLForm
    1788 | dock As HW2_DOCKForm
    1789 |  
    1790 | mrkr() As HW2_MRKRForm
    1791 |  
    1792 | bndv As HW2_BNDVForm
    1793 |  
    1794 | cold() As HW2_COLDForm
    1795 | bsrm() As HW2_BSRMForm
    1796 | End Type
    1797 | Public Type HW2_WARNForm
    1798 | ID As String * 4
    1799 | formLength As Long
    1800 |  
    1801 | lenContents As Long
    1802 | contents As String
    1803 | End Type
    1804 | Public Type HW2_ERRRForm
    1805 | ID As String * 4
    1806 | formLength As Long
    1807 |  
    1808 | lenContents As Long
    1809 | contents As String
    1810 | End Type
    1811 | Public Type HW2_OWNRForm
    1812 | ID As String * 4
    1813 | formLength As Long
    1814 |  
    1815 | lenContents As Long
    1816 | contents As String
    1817 | End Type
    1818 | Public Type HW2_INFOForm
    1819 | ID As String * 4
    1820 | formLength As Long
    1821 |  
    1822 | infoID As String * 4
    1823 |  
    1824 | warnings() As HW2_WARNForm
    1825 | errors() As HW2_ERRRForm
    1826 | owner As HW2_OWNRForm
    1827 | End Type
    1828 | Const HW2_MULTIM = "Homeworld2 Multi Mesh File"
    1829 | Const HW2_SIMPM = "Homeworld2 Simple Mesh File"
    1830 | Const HW2_WIREM = "Homeworld2 Wireframe Mesh File"
    1831 | 'Const HW2_BASICM = "Homeworld2 Basic File"
    1832 | 'Const HW2_PROGRESSIVEM = "Homeworld2 Variable Mesh File"
    1833 | Const HW2_VERSIONID = "VERS"
    1834 | Const HW2_BBOXID = "BBOX"
    1835 | Const HW2_BSPHID = "BSPH"
    1836 | Const HW2_TRISID = "TRIS"
    1837 | Const HW2_FORMID = "FORM"
    1838 | Const HW2_NAMEID = "NAME"
    1839 | Const HW2_STATID = "STAT"
    1840 | Const HW2_LMIPID = "LMIP"
    1841 | Const HW2_BMSHID = "BMSH"
    1842 | Const HW2_MULTID = "MULT"
    1843 | Const HW2_NRMLID = "NRML"
    1844 | Const HW2_DESCID = "DESC"
    1845 | Const HW2_VNRMID = "VNRM"
    1846 | Const HW2_SSUBID = "SSUB"
    1847 | Const HW2_DTRMID = "DTRM"
    1848 | Const HW2_HVMDID = "HVMD"
    1849 | Const HW2_HIERID = "HIER"
    1850 | Const HW2_ETSHID = "ETSH"
    1851 | Const HW2_GLOWID = "GLOW"
    1852 | Const HW2_MRKRID = "MRKR"
    1853 | Const HW2_INFOID = "INFO"
    1854 | Const HW2_BURNID = "BURN"
    1855 | Const HW2_COLDID = "COLD"
    1856 | Const HW2_NAVLID = "NAVL"
    1857 | Const HW2_DOCKID = "DOCK"
    1858 | Const HW2_KEYFID = "KEYF"
    1859 | Const HW2_BNDVID = "BNDV"
    1860 | Const HW2_ANIMID = "ANIM"
    1861 | Const HW2_BSRMID = "BSRM"
    1862 | Const HW2_WARNID = "WARN"
    1863 | Const HW2_ERRRID = "ERRR"
    1864 | Const HW2_OWNRID = "OWNR"
    1865 | Const HW2_GOBGID = "GOBG"
    1866 | 'Const HW2_GOBSID = "GOBS"
    1867 | 'Const HW2_GOBDID = "GOBD"
    1868 | 'Const HW2_HARDID = "HARD"
    1869 | 'Const HW2_GOBDID = "DAMG"
    1870 | Const HW2_MULTIMESH_BMSH_DWORDSPERVTX As Long = 27
    1871 | Const HW2_MULTIMESH_BMSH_VERSION As Long = 1400
    1872 | Const HW2_MRKR_HEAD_VERSION As Long = 1
    1873 | Const HW2_NAVL_VERSION As Long = 3
    1874 | Const HW2_DOCK_GLOBAL_PROPERTIES As Long = 6
    1875 | Const HW2_DOCK_LOCAL_PROPERTIES As Long = 10
    1876 | Const HW2_MULTIMESH_MULT_VERSION As Long = HW2_MULTIMESH_BMSH_VERSION
    1877 | Const HW2_MULTIMESH_GOBG_VERSION As Long = 1000
    1878 | Const HW2_MULTIMESH_GLOW_VERSION As Long = 1000
    1879 | Const HW2_BBOX_VERSION As Long = 1000
    1880 | Const HW2_BSPH_VERSION As Long = 1000
    1881 | Const HW2_TRIS_VERSION As Long = 1000
    1882 | Const HW2_DESC_VERSION As Long = 1000
    1883 | Const HW2_VNRM_VERSION As Long = 1000
    1884 | Const HW2_SSUB_VERSION As Long = 1000
    1885 | Const HW2_FACESTRIP As Long = 518
    1886 | Const HW2_FACELIST As Long = 514
    1887 |
    1888 |
    1889 |
    1890 | 1891 | 1892 |
    1893 |
    1894 | 1895 |
    1896 |
    1897 |
    1898 |
    1899 | 1900 |
    1901 | 1902 | 1903 | 1904 | 1905 | 1906 | 1907 | 1908 | 1909 | 1910 | 1911 | 1912 | 1913 | 1914 | 1915 | 1916 | 1917 | 1918 | 1919 | 1920 | 1921 | 1922 | 1923 | 1924 | 1925 | 1926 |
    1927 |
    1928 |
    1929 |
  4. 1930 | 1931 |
  5. 1932 | 1933 | 1934 |
    1935 | 1936 | 1941 | 1942 | 1943 | 1944 | 1945 | 1946 | #3 1947 | 1948 | 1949 | 1950 | 1951 | 1952 |
    1953 |
    1954 |
    1955 |
    1956 | 1957 |
    1958 | 4E534B 1959 | 1991 |
    1992 | 4E534B is offline 1993 | 1994 | 1995 |
    1996 | 1997 | Senior Member 1998 | 1999 | 2000 | 2001 | 2002 | 2003 | 2004 |
    2005 |
    2006 |
    Join Date
    May 2006
    2007 | 2008 | 2009 | 2010 | 2011 |
    2012 | 2013 | 2014 | 2017 | 2018 |
    2019 |
    2020 |
    2021 | 2022 | 2023 | 2024 | 2025 | 2026 | 2027 | 2028 | 2029 |
    2030 |
    2031 |
    2032 | *BIIIIIG BIIIIIG AND EVEN BIIIGER BUMP*
    2033 | The HOD format now gives a complete HOD format for HW2 HODs
    2034 | ( I think that LMTX is a chunk that's not used [ because I couln't find it anywhere ]
    2035 | GOBD, GOBS are archaic goblin formats, not used, and I don't think anything else is left ) 2036 |
    2037 |
    2038 | 2039 | 2040 |
    2041 |
    2042 | 2043 |
    2044 |
    2045 |
    2046 |
    2047 | 2048 |
    2049 | 2050 | 2051 | 2052 | 2053 | 2054 | 2055 | 2056 | 2057 | 2058 | 2059 | 2060 | 2061 | 2062 | 2063 | 2064 | 2065 | 2066 | 2067 | 2068 | 2069 | 2070 | 2071 | 2072 | 2073 | 2074 |
    2075 |
    2076 |
    2077 |
  6. 2078 | 2079 |
2080 |
2081 |
2082 | 2083 |
2084 | 2085 | 2086 | 2087 |
2088 | 2089 |
2090 | 2091 |
2092 | 2093 |
2094 | 3082 |
3083 |
3084 |
3085 | 3086 | 3087 | 3088 | 3089 | 3090 | 3091 | 3092 | 3093 | 3094 | 3095 | 3096 | 3106 | 3107 | 3108 |
3109 | 3110 |

Thread Information

3111 |
3112 |
3113 |
Users Browsing this Thread
3114 |
3115 |

There are currently 1 users browsing this thread. (0 members and 1 guests)

3116 |
    3117 |   3118 |
3119 |
3120 |
3121 |
3122 | 3123 | 3124 | 3125 | 3126 |
3127 | 3128 |
3129 |

3130 | 3131 | Posting Permissions 3132 |

3133 |
3134 | 3135 |
3136 | 3137 |
    3138 |
  • You may not post new threads
  • 3139 |
  • You may not post replies
  • 3140 |
  • You may not post attachments
  • 3141 |
  • You may not edit your posts
  • 3142 |
  •  
  • 3143 |
3144 |
3145 | 3152 |
3153 | 3154 | 3155 |
3156 | 3157 |
3158 |
3159 | 3160 |
3161 | 3162 |
3163 | 3164 | 3165 | 3166 | 3167 | 3168 | 3169 | 3170 | 3171 |
3172 | 3173 | 3193 |
3194 | 3195 | 3259 |
3260 | 3261 |
3262 | 3263 | 3264 | 3269 | 3276 | 3277 | 3278 |
3279 | 3280 | --------------------------------------------------------------------------------