Created
January 4, 2022 15:27
-
-
Save pwillard/cd840fff2407b4d318c5506ce6aa0296 to your computer and use it in GitHub Desktop.
This is a modification of the Blender MSTS/ORTS S file Exporter to allow you to choose between an ACE file or DDS file reference for all textures
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bl_info = { "name": "Export OpenRails/MSTS Shape File(.s)", | |
"author": "Wayne Campbell", | |
"version": (4, 3), | |
"blender": (2, 80, 0), | |
"location": "File > Export > OpenRails/MSTS (.s)", | |
"description": "Export file to OpenRails/MSTS .S format", | |
"category": "Import-Export"} | |
''' | |
COPYRIGHT 2019 by Wayne Campbell | |
This program is free software: you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation, either version 3 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
A copy of the GNU General Public License is included in this distribution package | |
or may be found here <http://www.gnu.org/licenses/>. | |
For complete documentation, and CONTACT info see the Instructions included in the distribution package. | |
REVISION HISTORY | |
2019-07-23 Released V4.3 | |
Improved instructions around linking a part to multiple LOD collections | |
Fixed program error - no material slots, or empty material slot | |
2019-07-21 Released V4.2 for testing | |
Added HierarchyOptimization | |
Added FastExport - improves speed 25 x | |
Restructured AddMesh, AddTriangle with MSTSMaterialsDetail structure to improve performance | |
Indexed vertices for faster export ( eg speedup by 3 times ) | |
2019-07-18 Released V4.1 to beta testers | |
Modified install package to work with 'Install from file..' | |
Fixed ShapeViewer crashes on animations(0) | |
2019-07-17 Released V4.0 to beta testers | |
Updated for Blender 2.8 | |
2017-09-12 Released as V 3.5 | |
added SubObjID to SubObjectHeader per report from 'Spike' | |
added support for Auto Smoothing per http://www.elvastower.com/forums/index.php?/topic/30491-blender-auto-smooth-and-msts-export/ | |
2016-01-11 Released as V 3.4 | |
strip periods (.) from object names | |
fixes to LOD inheritance | |
remember RetainNames setting | |
file export dialog, default to .s extension | |
2016-01-10 Testing as V 3.3 | |
Added RetainNames option | |
Fixed bounding sphere bug | |
Trimmed most numbers to 6 decimal places to reduce output file size | |
2016-01-10 Testing as V 3.2 | |
Documented hidden option to use Railworks style LOD naming | |
Documented setting MAX values in Custom Properties | |
Fixed AttributeError: 'NoneType' object has no attribute 'game_settings' | |
2016-01-09 Testing as V 3.1 | |
Added MipMapLODBias setting | |
Fixed bug, untextured faces - AttributeError: 'NoneType' object has no attribute 'name' | |
Implemented MSTS sub_object flags for sorting alpha blended faces ( not needed for OR ) | |
Implement MSTS sub_object flags for specularity ( not needed for OR ) | |
Added Alpha Sorted Transparency option to MSTS Material | |
Improved descriptions and naming of MSTS Material settings | |
Fixed MSTS Lighting value not being saved | |
Sped up iVertexAdd, iNormalAdd, etc, with spatial tree indexing, etc | |
2016-01-08 Testing as V 3.0 | |
Fix for WHEELS13, WHEELS23 | |
Changed use_shadeless, from selecting Cruciform, now done with MSTS Lighting | |
Added MSTS Material panel for lighting options, etc | |
Added support for Railworks style LOD naming | |
2015-11-11 Released as V 2 | |
Changed prim_state labels to be compatible with Polymaster | |
2013-11-01 Initial Release as V 2.6.3 | |
TODO FUTURE | |
- add a progress bar ( when available in the Blender API ) | |
- document or remove hidden Normals override options ( superceded by Blender's new Normals Modifier ) | |
IDEAS FUTURE | |
- add support for curves, particle systems, dupliverts | |
- support undocumented MSTS capability, eg | |
- Wrap, Clamp, Extend etc | |
- double sided faces | |
- bump mapping and environmental reflections | |
- AddATex, SubractATex, etc and other undocumented shaders | |
- zBias | |
- use of lod_control objects to improve LOD efficiency | |
- option to export texture files | |
- option to compress shape file | |
- options to generate .SD, .ENG, .WAG or .REF | |
''' | |
import bpy | |
import os | |
from math import radians | |
from bpy.props import StringProperty, EnumProperty, BoolProperty | |
import mathutils | |
from mathutils import * | |
class MyException( Exception ): | |
pass | |
MaxVerticesPerPrimitive = 8000 # 8000 OK 20000 Fails | |
MaxVerticesPerSubObject = 15000 # 15000 OK 20000 Fails, Note: Spike reports 14000 failed, he uses 12000 | |
FastExport = True # do less compaction to reduce export time at cost of slightly larger file size | |
# no impact on frame rates for most files, very large files may see a couple of additional Draw calls | |
# eg L1-huge.blend exports in 28 sec vs 11 min 42 sec, file size = 63,628,192 vs 55,423,130 bytes | |
HierarchyOptimization = True # gives better triangle fill, reduces subobject splitting in middle of primitive, | |
# creates new subobjects for each hierarchy level | |
# should improve frame rates by reducing Draw Calls, and creating smaller vertex sets, where possible | |
# yet places multiple primitives in a subobject when vertex sharing across large vertex sets make sense | |
RetainNames = False # user option, when true, the exporter disables mesh | |
# consolidation and hierarchy collapse optimizations | |
# reduces frame rates due to more Draw Calls | |
UseDDS = False | |
##################################### | |
from bpy_extras.io_utils import ExportHelper, ImportHelper # gives access to the FileSelectParams | |
ProgressIndicator = 0.0 # spins from 0 to 1 | |
ProgressContext = None | |
def UpdateProgress(): # this is the little cursor counter progress indicator ( all thats available in Blender's API ) | |
global ProgressIndicator | |
global ProgressContext | |
ProgressContext.window_manager.progress_update(ProgressIndicator) | |
ProgressIndicator += 0.0001 | |
if ProgressIndicator > 1.0: | |
ProgressIndicator = 0 | |
class ExportHelper: | |
filepath : StringProperty( | |
name="File Path", | |
description="Filepath used for exporting the file", | |
maxlen=1024, | |
subtype='FILE_PATH', | |
) | |
# set up the FileSelectParams | |
filename_ext = ".s" | |
filter_glob : StringProperty(default="*.s", options={'HIDDEN'}) | |
class MSTSExporter(bpy.types.Operator, ExportHelper): | |
bl_idname = "export.msts_s" | |
bl_description = 'Export to OpenRails/MSTS .s file' | |
bl_label = "Export OpenRails/MSTS S" | |
bl_space_type = "PROPERTIES" | |
bl_region_type = "WINDOW" | |
bl_options = {'UNDO'} | |
filepath : StringProperty(subtype='FILE_PATH') | |
def invoke(self, context, event): | |
settings = context.scene.msts | |
# set up default path | |
blend_filepath = context.blend_data.filepath | |
if not blend_filepath: | |
blend_filepath = "untitled" | |
else: | |
blend_filepath = os.path.splitext(blend_filepath)[0] | |
self.filepath = blend_filepath + self.filename_ext | |
# override with last used path if it looks good | |
if settings.SFilepath != "": | |
lastSavedPath = os.path.abspath( bpy.path.abspath( settings.SFilepath ) ) | |
lastSavedFolder = os.path.split(lastSavedPath)[0] | |
if os.path.exists( lastSavedFolder ): | |
self.filepath = lastSavedPath | |
WindowManager = context.window_manager | |
WindowManager.fileselect_add(self) | |
return {"RUNNING_MODAL"} | |
def draw( self, context ): | |
settings = context.scene.msts | |
layout = self.layout | |
layout.prop( self, "filepath" ) | |
layout.prop( settings, "RetainNames" ) | |
layout.prop( settings, "UseDDS" ) | |
def execute(self, context): | |
settings = context.scene.msts | |
global RetainNames | |
RetainNames = settings.RetainNames | |
global UseDDS | |
UseDDS = settings.UseDDS | |
#Append .s | |
exportPath = bpy.path.ensure_ext(self.filepath, ".s") | |
settings.SFilepath = TryGetRelPath( exportPath ) | |
# Validate root object | |
rootName = 'MAIN' | |
rootCollection = bpy.context.scene.collection.children.get( rootName ) | |
if rootCollection == None: | |
print() | |
print( "ERROR: MAIN not found in Scene Collection.") | |
self.report( {'ERROR'}, "MAIN not found in Scene Collection" ) | |
return {'CANCELLED' } | |
#force out of edit mode | |
if bpy.context.mode != 'OBJECT': | |
bpy.ops.object.mode_set( mode = 'OBJECT' ) | |
#Export | |
print() | |
print( "EXPORTING "+rootName + " TO " + exportPath ) | |
print() | |
global ProgressContext | |
ProgressContext = context | |
context.window_manager.progress_begin( 0,1 ) # progress bar indication ( forthcoming feature ) | |
UpdateProgress() | |
try: | |
ExportShapeFile( rootName, exportPath ) | |
print( "FINISHED OK" ) | |
self.report( {'INFO'}, "Finished OK" ) | |
return {"FINISHED"} | |
except MyException as error: # when we raise an error it comes here | |
context.window_manager.progress_end() | |
print( "ERROR: " + ' '.join(error.args) ) | |
self.report( {'WARNING'}, ' '.join(error.args) ) | |
return { "CANCELLED" } | |
# all other exceptions are passed up for traceback | |
finally: | |
context.window_manager.progress_end() | |
def cancel( self, message ): | |
print( "ERROR: ", message ) | |
self.report( {'ERROR'}, message ) | |
return {'CANCELLED' } | |
def menu_func(self, context): | |
self.layout.operator(MSTSExporter.bl_idname, text="OpenRails/MSTS (.s)") | |
def register(): | |
bpy.utils.register_class( msts_material_props) # define the new msts material properties | |
bpy.utils.register_class( msts_scene_props) | |
bpy.types.Material.msts = bpy.props.PointerProperty(type=msts_material_props) # add the properties to the material type | |
bpy.types.Scene.msts = bpy.props.PointerProperty(type=msts_scene_props) | |
bpy.utils.register_class( msts_material_panel ) # add a UI panel to the Materials window | |
bpy.utils.register_class( MSTSExporter ) # define the exporter dialog panel | |
bpy.types.TOPBAR_MT_file_export.append(menu_func) # add the exporter to the menu | |
def unregister(): | |
bpy.types.TOPBAR_MT_file_export.remove(menu_func) | |
bpy.utils.unregister_class( MSTSExporter ) | |
bpy.utils.unregister_class( msts_material_panel ) | |
del bpy.types.Scene.msts | |
del bpy.types.Material.msts | |
bpy.utils.unregister_class( msts_scene_props) | |
bpy.utils.unregister_class( msts_material_props ) | |
''' | |
************************ THE GUI ******************************* | |
''' | |
##################################### | |
# find an image with the specified path, or create it | |
def GetImage( filepath ): | |
return bpy.data.images.load( filepath, check_existing = True ) | |
##################################### | |
# recreate the shader nodes to represent the msts settings | |
def RecreateShaderNodes( m ): | |
# remove any existing nodes | |
m.use_nodes = True | |
m.node_tree.nodes.clear() | |
m.node_tree.links.clear() | |
# create the nodes | |
MNode = m.node_tree.nodes.new('ShaderNodeOutputMaterial') | |
BNode = m.node_tree.nodes.new('ShaderNodeBsdfPrincipled') | |
TNode = m.node_tree.nodes.new('ShaderNodeTexImage') | |
UNode = m.node_tree.nodes.new('ShaderNodeUVMap') | |
# layout the shader screen | |
MNode.location.x = 300 | |
BNode.location.x = 0 | |
TNode.location.x = -300 | |
UNode.location.x = -550 | |
# Set active node | |
MNode.select = False | |
BNode.select = False | |
TNode.select = False | |
UNode.select = False | |
m.node_tree.nodes.active = None | |
# setup specularity | |
if m.msts.Lighting == 'SPECULAR25': | |
BNode.inputs['Specular'].default_value = 0.1 | |
BNode.inputs['Roughness'].default_value = 0.3 | |
elif m.msts.Lighting == 'SPECULAR750': | |
BNode.inputs['Specular'].default_value = 0.1 | |
BNode.inputs['Roughness'].default_value = 0.1 | |
else: | |
BNode.inputs['Specular'].default_value = 0.0 | |
BNode.inputs['Roughness'].default_value = 1.0 | |
# setup image | |
if os.path.exists( bpy.path.abspath(m.msts.BaseColorFilepath) ): | |
TNode.image = GetImage( m.msts.BaseColorFilepath ) | |
# setup uvs | |
UNode.uv_map = 'UVMap' | |
# link the nodes | |
m.node_tree.links.new(MNode.inputs['Surface'],BNode.outputs['BSDF']) | |
m.node_tree.links.new(BNode.inputs['Base Color'],TNode.outputs['Color']) | |
if m.msts.Transparency != "OPAQUE": | |
m.node_tree.links.new(BNode.inputs['Alpha'],TNode.outputs['Alpha']) | |
m.node_tree.links.new(TNode.inputs['Vector'],UNode.outputs['UV']) | |
# TODO Add support for additional msts lighting modes, eg cruciform, etc | |
##################################### | |
# called when the MSTS material panel settings are changed | |
def SetDefaultViewportShading( screenName ): | |
for screen in bpy.data.screens: | |
if screen.name == screenName: | |
area = next(area for area in screen.areas if area.type == 'VIEW_3D') | |
space = next(space for space in area.spaces if space.type == 'VIEW_3D') | |
space.shading.show_backface_culling = True | |
space.shading.color_type = 'TEXTURE' | |
space.shading.light = 'STUDIO' | |
return | |
print( "Warning: Screen " + screenName + " not found while setting viewport shading." ) | |
# Show specified image in active UV window | |
def SetUVWindowImage( context, image ): | |
if image != None: | |
for eachArea in context.screen.areas: | |
if eachArea.type == 'IMAGE_EDITOR': | |
for eachSpace in eachArea.spaces: | |
if eachSpace.type == 'IMAGE_EDITOR': | |
eachSpace.image = image | |
return | |
return | |
# Show specified image path in active UV window | |
def SetUVWindowImagePath( context, imagePath ): | |
if os.path.exists( bpy.path.abspath(imagePath) ): | |
image = GetImage( imagePath ) | |
SetUVWindowImage( context, image ) | |
##################################### | |
# called when the image is changed in the MSTS material panel | |
# changes material name to match and updates material settings | |
def UpdateMSTSImage( self, context ): | |
if hasattr(context, 'material'): | |
if context.material != None: | |
mstsmaterial= context.material.msts | |
if mstsmaterial.UpdateNodes: | |
materialName = bpy.path.display_name_from_filepath( mstsmaterial.BaseColorFilepath ) | |
if not materialName in bpy.data.materials: | |
context.material.name = materialName | |
#else: | |
# if the suggested material name already exists, | |
# then leave it up to the user to sort it out | |
SetUVWindowImagePath( context, mstsmaterial.BaseColorFilepath ) | |
UpdateMSTSMaterial( self, context ) | |
##################################### | |
# called when the MSTS material panel settings are changed | |
def UpdateMSTSMaterial( self, context ): | |
if hasattr(context, 'material'): | |
if context.material != None: | |
mstsmaterial= context.material.msts | |
if mstsmaterial.UpdateNodes: | |
blM = context.material | |
blM.use_backface_culling = True | |
if blM.msts.Transparency == "OPAQUE": | |
blM.blend_method = "OPAQUE" | |
elif blM.msts.Transparency == "CLIP": | |
blM.blend_method = "CLIP" | |
elif blM.msts.Transparency == "ALPHA": | |
blM.blend_method = "BLEND" | |
elif blM.msts.Transparency == "ALPHA_SORT": | |
blM.blend_method = "BLEND" | |
RecreateShaderNodes( blM ) | |
SetDefaultViewportShading( 'Layout' ) | |
SetDefaultViewportShading( 'UV Editing' ) | |
##################################### | |
class msts_material_panel(bpy.types.Panel): | |
"""Creates a Panel in the material context of the properties editor""" | |
bl_label = "MSTS Materials" | |
bl_idname = "MATERIAL_PT_msts" | |
bl_space_type = 'PROPERTIES' | |
bl_region_type = 'WINDOW' | |
bl_context = "material" | |
def draw(self, context): # see bpy.context for info on available context's | |
if context.material != None: | |
mstsmaterial= context.material.msts | |
layout = self.layout | |
layout.use_property_split = True | |
layout.use_property_decorate = False | |
# Create a simple row. | |
col = layout.column(align = True ) | |
col.prop(mstsmaterial, property="Transparency" ) | |
col.prop(mstsmaterial, property="Lighting" ) | |
col.prop(mstsmaterial, property="MipMapLODBias") # this should be a texture property | |
# not a material property | |
# but here is simpler for the user to find | |
col.prop(mstsmaterial, property="BaseColorFilepath" ) | |
col.prop(mstsmaterial, property="UpdateNodes" ) | |
##################################### | |
class msts_material_props(bpy.types.PropertyGroup): | |
# define custom material properties, ie material.msts.shadername note: these prop's will be saved in the blend file | |
Transparency : bpy.props.EnumProperty( | |
name="Transparency", | |
description="Controls handling of alpha channel.", | |
items = [ ( "OPAQUE", "Solid Opaque", "Alpha channel is ignored" ), | |
( "CLIP", "Transparency On/Off", "Transparent if alpha value below a threshold" ), | |
( "ALPHA", "Alpha Blended", "Alpha value blends from transparent up to opaque" ), | |
( "ALPHA_SORT","Alpha Sorted", "Alpha blending with depth sort" ) | |
], | |
default="OPAQUE", | |
update= UpdateMSTSMaterial, | |
) | |
Lighting : bpy.props.EnumProperty( | |
name="Lighting", | |
description="Adjusts light and shadow", | |
items = [ ("NORMAL", "Normal", "Sun facing surfaces are lit and opposite are shaded" ), | |
("SPECULAR25", "Specular 25", "Strong specular highlight" ), | |
("SPECULAR750", "Specular 750", "Small specular highlight" ), | |
("FULLBRIGHT", "Full Bright", "Shaded surfaces appear lit" ), | |
("HALFBRIGHT", "Half Bright", "Shaded surfaces appear partly lit" ), | |
("DARK", "Dark", "Sun facing surfaces appear fully shaded" ), | |
("CRUCIFORM", "Cruciform", "Indirect ambient lighting only" ), | |
("EMISSIVE", "Emissive", "Surfaces emit light at night" ) | |
], | |
default="NORMAL", | |
update= UpdateMSTSMaterial, | |
) | |
MipMapLODBias : bpy.props.FloatProperty( | |
name="Mip Bias", | |
description="Controls sharpness of the image, default = -3", | |
default = 0, | |
soft_max = 8, | |
soft_min = -8, | |
precision = 1, | |
step = 100 | |
) | |
BaseColorFilepath : StringProperty( | |
description = 'The texture image file path.', | |
update= UpdateMSTSImage, | |
subtype='FILE_PATH', | |
) | |
UpdateNodes : BoolProperty( | |
name='Update Shader', | |
description = 'Change shader nodes to match these settings', | |
default = True , | |
update= UpdateMSTSMaterial, | |
) | |
LightingOptions = { "NORMAL":-5, | |
"SPECULAR25":-6, | |
"SPECULAR750":-7, | |
"FULLBRIGHT":-8, | |
"HALFBRIGHT":-11, | |
"DARK":-12, | |
"CRUCIFORM":-9, | |
"EMISSIVE":-5 | |
} | |
class msts_scene_props(bpy.types.PropertyGroup): | |
# these properties appear in the file export dialog | |
SFilepath : StringProperty( default = "" ) | |
RetainNames : BoolProperty(name='Retain Names', description = 'Disables object merging optimizations', default = False ) | |
UseDDS : BoolProperty(name='Use DDS', description = 'Export Texture type as DDS instead of ACE', default = False ) | |
''' | |
This code converts from Blender data structures to MSTS data structures | |
from here down to the Library section, the code uses blender coordinate system unless specified as MSTS | |
''' | |
##################################### | |
def GetFileNameNoExtension( filepath ): # eg filePath = '//textures\\redtile.tga' | |
s = filepath.replace( '\\','/' ) # s = '//textures/redtile.tga' | |
parts = s.split( '/' ) # parts = ['','','textures','redtile.tga'] | |
lastPart = parts[ len(parts)-1 ] # lastPart = 'redtile.tga' | |
noextension = os.path.splitext( lastPart )[0] # noextension = 'redtile' | |
return noextension | |
##################################### | |
# convert to path relative to this blend file if possible | |
# otherwise just return filepath | |
def TryGetRelPath( filepath ): | |
try: | |
return bpy.path.relpath( filepath ) | |
except: | |
return filepath | |
##################################### | |
# specifies how normals are to be calculated | |
class Normals: # passed to AddFaceToSubObject to request special handling of normals | |
Face = 0 # flat normals | |
Smooth = 1 # smoothed ( can be overriden per face ) | |
Out = 3 # normals radiate out from center of model Mesh or Material Property: NORMALS = OUT | |
Up = 4 # all normals face up Mesh or Material Property: NORMALS = UP | |
Fillet = 5 # bevels are modified to appear rounded Mesh or Material Property: NORMALS = FILLET | |
OutX = 6 # normals radiate out to the left and right along objects x=0,z=-4 axis | |
##################################### | |
# given a blender property override like NORMALS = UP, and an initialNormal, | |
# return the normal state after the override is applied | |
# propertyString represents eg "UP" | |
# initialNormal eg Normals.Face | |
def GetNormalsOverride( propertyString, initialNormal ): | |
if propertyString == 'UP': | |
return Normals.Up | |
elif propertyString == 'OUT': | |
return Normals.Out | |
elif propertyString == 'FILLET': | |
return Normals.Fillet | |
elif propertyString == "OUTX": | |
return Normals.OutX | |
return initialNormal | |
##################################### | |
def iShaderAdd( shaderName ): | |
if shaderName in ExportShape.Shaders: | |
iShader = ExportShape.Shaders.index(shaderName) | |
else: | |
iShader = len( ExportShape.Shaders ) | |
ExportShape.Shaders.append( shaderName ) | |
return iShader | |
##################################### | |
def iFilterAdd( filterName ): | |
if filterName in ExportShape.Filters: | |
iFilter = ExportShape.Filters.index( filterName ) | |
else: | |
iFilter = len( ExportShape.Filters ) | |
ExportShape.Filters.append( filterName ) | |
return iFilter | |
##################################### | |
# imageName is in msts format, eg unionstop.ace | |
def iImageAdd( imageName ): | |
if imageName==None or imageName == '': | |
imageName = 'blank' | |
if UseDDS==False: | |
imageName = imageName + '.ace' | |
else: | |
imageName = imageName + '.dds' | |
for iImage in range(0, len( ExportShape.Images)): | |
if ExportShape.Images[iImage] == imageName: | |
return iImage | |
iImage = len( ExportShape.Images) | |
ExportShape.Images.append( imageName) | |
return iImage | |
##################################### | |
def iTextureAdd( imageName, mipMapLODBias ): # creates both texture and image entries | |
iImage = iImageAdd( imageName ) | |
for iTexture in range( 0, len( ExportShape.Textures)): | |
texture = ExportShape.Textures[iTexture] | |
if texture.iImage == iImage and texture.MipMapLODBias == mipMapLODBias: | |
return iTexture | |
iTexture = len( ExportShape.Textures ) | |
newTexture = Texture() | |
newTexture.iImage = iImage | |
newTexture.iFilter = iFilterAdd( 'MipLinear' ) | |
newTexture.MipMapLODBias = mipMapLODBias | |
ExportShape.Textures.append( newTexture ) | |
return iTexture | |
##################################### | |
def UVOpsMatch( opsList, specifierList ): # specifiers is a list ( operation, textureAddressMode ) pairs | |
if len( opsList ) != len( specifierList ): | |
return False | |
for i in range( 0, len( opsList ) ): | |
eachUVOp = opsList[i] | |
operation = specifierList[i][0] | |
if eachUVOp.__class__ != operation: return False | |
if operation == UVOpCopy and eachUVOp.TextureAddressMode != specifierList[i][1]: return False | |
elif operation == UVOpReflectMapFull: continue | |
return True | |
##################################### | |
def iLightConfigAdd( uvops ): # it a list of ( operation, textureAddressMode ) pairs | |
# see if its already set up | |
for i in range( 0, len( ExportShape.LightConfigs ) ): | |
if UVOpsMatch( ExportShape.LightConfigs[i].UVOps, uvops ): | |
return i | |
# no, so create it | |
i = len( ExportShape.LightConfigs ) | |
newLightConfig = LightConfig() | |
for eachop in uvops: | |
if eachop[0] == UVOpCopy: | |
newUVOp = UVOpCopy() | |
newUVOp.TextureAddressMode = eachop[1] | |
elif op[0] == UVOpReflectMapFull: | |
newUVOp = UVOpReflectMapFull() | |
else: | |
raise Exception( "PROGRAM ERROR: UNDEFINED UV OPERATION" ) | |
newLightConfig.UVOps.append( newUVOp ) | |
ExportShape.LightConfigs.append( newLightConfig ) | |
return i | |
##################################### | |
def iColorAdd( color ): # a r g b | |
global UniqueColors | |
return UniqueColors.IndexOf( color ) | |
##################################### | |
def iLightMaterialAdd( lm ): # diff, amb, spec, emmisive, power | |
global UniqueLightMaterials | |
return UniqueLightMaterials.IndexOf( lm ) | |
##################################### | |
def iVertexStateAdd( flags, iMatrix, iLightMaterial, iLightConfig ): | |
i = len( ExportShape.VertexStates ) # use the last correct entry - earlier ones will have a full vtx_set | |
while i > 0: | |
i -= 1 | |
eachVertexState = ExportShape.VertexStates[i] | |
if eachVertexState.Flags == flags \ | |
and eachVertexState.iMatrix == iMatrix \ | |
and eachVertexState.iLightMaterial == iLightMaterial \ | |
and eachVertexState.iLightConfig == iLightConfig: | |
# we found one | |
return i | |
# we didn't find it, so add it and add a corresponding vertex set to every sub_object | |
i = len( ExportShape.VertexStates ) | |
newVertexState = VertexState() | |
newVertexState.Flags = flags | |
newVertexState.iMatrix = iMatrix | |
newVertexState.iLightMaterial = iLightMaterial | |
newVertexState.iLightConfig = iLightConfig | |
ExportShape.VertexStates.append( newVertexState ) | |
# every vertex_state needs a matching vertex_set | |
for lodControl in ExportShape.LodControls: | |
for distanceLevel in lodControl.DistanceLevels: | |
for subobject in distanceLevel.SubObjects: | |
subobject.VertexSets.append( VertexSet() ) #Note: unused vertexSets are purged during write | |
return i | |
##################################### | |
def MatchList( lista, listb ): | |
if len(lista) != len(listb): | |
return False | |
for i in range( 0, len(lista) ): | |
if lista[i] != listb[i]: | |
return False | |
return True | |
##################################### | |
def ColorWord( floats ): | |
value = 0; | |
for f in floats: | |
value = value * 256 | |
value = value + round( f * 255 ) | |
return value | |
##################################### | |
def IsLinkedToBaseColor( imageNode ): | |
for eachLink in imageNode.outputs['Color'].links: | |
if eachLink.to_socket.name == 'Base Color' or \ | |
eachLink.to_socket.name == 'Color': | |
return True | |
return False | |
##################################### | |
# return an msts format image name, eg unionstop.ace | |
def BaseColorImageFrom( material ): | |
if material == None: | |
return None | |
# if we specified a filepath in MSTS Material panel, use it | |
if material.msts.BaseColorFilepath != '': | |
return GetFileNameNoExtension( material.msts.BaseColorFilepath ) | |
# Otherwise, look for an image in the shader node tree linked to a Color or Base Color input | |
if material.node_tree != None: | |
for eachNode in material.node_tree.nodes: | |
if eachNode.bl_idname == 'ShaderNodeTexImage': | |
if eachNode.image.source == 'FILE': | |
if IsLinkedToBaseColor( eachNode ): | |
return GetFileNameNoExtension( eachNode.image.filepath ) | |
return None | |
##################################### | |
def iPrimStateAdd( iVertexState, zBias, iShader, alphaTestMode, iLightConfig, iTextures ): | |
global ExportShape | |
# see if this one's already set up | |
for i in range( 0, len( ExportShape.PrimStates ) ): | |
eachPrimState = ExportShape.PrimStates[i] | |
if eachPrimState.iVertexState == iVertexState \ | |
and eachPrimState.zBias == zBias \ | |
and eachPrimState.iShader == iShader \ | |
and eachPrimState.AlphaTestMode == alphaTestMode \ | |
and eachPrimState.iLightConfig == iLightConfig \ | |
and MatchList( eachPrimState.iTextures, iTextures ): | |
return i | |
# we didn't find it so add it | |
i = len( ExportShape.PrimStates ) | |
newPrimState = PrimState() | |
iHierarchy = ExportShape.VertexStates[iVertexState].iMatrix | |
newPrimState.Label = ExportShape.Matrices[iHierarchy].Label | |
if len( iTextures) > 0: | |
texture = ExportShape.Textures[iTextures[0]] | |
imagename = ExportShape.Images[texture.iImage] | |
newPrimState.Label = newPrimState.Label + '_' + os.path.splitext( imagename)[0] | |
for iTexture in iTextures: | |
newPrimState.iTextures.append(iTexture) | |
newPrimState.zBias = zBias | |
newPrimState.iVertexState = iVertexState | |
newPrimState.iShader = iShader | |
newPrimState.AlphaTestMode = alphaTestMode | |
newPrimState.iLightConfig = iLightConfig | |
ExportShape.PrimStates.append( newPrimState ) | |
return i | |
##################################### | |
def iPrimitiveAppend( subObject, iPrimState ): | |
i = len( subObject.Primitives ) | |
newPrimitive = Primitive() | |
newPrimitive.iPrimState = iPrimState | |
subObject.Primitives.append( newPrimitive ) | |
return i | |
##################################### | |
def iPrimitiveAdd( subObject, iPrimState ): | |
i = len( subObject.Primitives ) | |
while i > 0: # use the last correct entry - earlier ones could be full | |
i -= 1 | |
primitive = subObject.Primitives[i] | |
if primitive.iPrimState == iPrimState: | |
return i | |
#we didn't find it, so add a new one | |
i = iPrimitiveAppend( subObject, iPrimState ) | |
return i | |
##################################### | |
class UniqueArray: | |
def __init__(self, data, hash, tolerance ): | |
self.data = data | |
self.keys = {} | |
self.hash = hash | |
self.tolerance = tolerance | |
def __getitem__(self, i): | |
return self.data[i] | |
def Match( self, v1, v2 ): | |
for i in range( 0, len( v1 ) ): | |
if abs( v1[i] - v2[i] ) > self.tolerance: | |
return False | |
return True | |
def Key( self, value ): | |
key = 0.0 | |
for v in value: | |
key += v | |
return round(key,self.hash) | |
def IndexOf( self, value ): | |
key = self.Key( value ) | |
index = self.keys.get(key,-1) | |
while index != -1: | |
storedValue = self.data[index] | |
if self.Match( value, storedValue): | |
return index | |
key += .9 # in case of a hash collision, we advance the key this amount | |
index = self.keys.get(key,-1) | |
# Performance improvement, live with some duplicate values to speed up export | |
global FastExport | |
if FastExport: | |
if len( self.keys ) > 4000: | |
self.keys.clear() | |
# we didn't find it so add it | |
index = len( self.data ) | |
self.data.append( value ) | |
self.keys[ key ] = index | |
return index | |
##################################### | |
def iVertexAdd( iPoint, iNormal, iUVs, vertexSet, color1, color2 ): | |
# search index to see if a matching vertex exists | |
if iPoint in vertexSet.index: | |
for iVertex in vertexSet.index[iPoint]: | |
vertex = vertexSet.Vertices[iVertex] | |
if vertex.iPoint == iPoint and vertex.iNormal == iNormal and MatchList(vertex.iUVs,iUVs) and vertex.Color1 == color1 and vertex.Color2 == color2 : | |
return iVertex | |
#we didnt' find it so add a new one | |
vertex = Vertex() | |
vertex.iPoint = iPoint | |
vertex.iNormal = iNormal | |
vertex.Color1 = color1 | |
vertex.Color2 = color2 | |
for iUV in iUVs: | |
vertex.iUVs.append( iUV ) | |
iVertex = len( vertexSet.Vertices ) | |
vertexSet.Vertices.append( vertex ) | |
#index on iPoint for speedup | |
if not iPoint in vertexSet.index: | |
vertexSet.index[iPoint] = [] | |
vertexSet.index[iPoint].append( iVertex ) | |
return iVertex | |
##################################### | |
def iUVPointAdd( uvPoint ): | |
global UniqueUVPoints | |
MSTSuvPoint = (uvPoint[0],1-uvPoint[1]) | |
return UniqueUVPoints.IndexOf( MSTSuvPoint ) | |
##################################### | |
def iNormalAdd( vector ): | |
global UniqueNormals | |
MSTSvector = (vector[0],vector[2],vector[1] ) | |
return UniqueNormals.IndexOf( MSTSvector ) | |
##################################### | |
# When a subObject is full, create a new empty | |
def SplitSubObject( subObject): | |
newSubObject = SubObject( subObject.DistanceLevel ) | |
# DEBUG print( 'split subObject ',subObject.sequence,' into ',newSubObject.sequence ) | |
newSubObject.Flags = subObject.Flags | |
newSubObject.Priority = subObject.Priority | |
newSubObject.iHierarchy = subObject.iHierarchy | |
for eachVertexSet in subObject.VertexSets: | |
newVertexSet = VertexSet() | |
newSubObject.VertexSets.append( newVertexSet ) #unused vertex sets are purged during write | |
subObject.DistanceLevel.SubObjects.append( newSubObject ) | |
return newSubObject | |
######################################## | |
# find the last subobject that uses the specified flags | |
# or return None of none found | |
def FindSubObject( distanceLevel, flags, priority, iHierarchy): | |
# starting at the end, look for one with the needed flags | |
iLast = len( distanceLevel.SubObjects ) - 1 | |
while iLast >= 0: | |
subObject = distanceLevel.SubObjects[iLast] | |
if HierarchyOptimization: | |
if subObject.Flags == flags and subObject.Priority == priority and subObject.iHierarchy == iHierarchy: | |
return subObject | |
else: | |
if subObject.Flags == flags and subObject.Priority == priority: | |
return subObject | |
iLast -= 1 | |
return None | |
##################################### | |
def ChildOf( ancestor, object ): | |
if object.parent == None: | |
return False | |
if object.parent == ancestor: | |
return True | |
if ChildOf( ancestor, object.parent ): | |
return True | |
return False | |
##################################### | |
def ConstructMatrix( ancestor, object ): | |
# construct a cumulative transfer matrix from object to ancestor | |
if object == ancestor: | |
return mathutils.Matrix.Translation((0,0,0)) | |
if object.parent == None: | |
return object.matrix_local | |
if object.parent == ancestor: | |
return object.matrix_local | |
return ConstructMatrix( ancestor, object.parent ) @ object.matrix_local | |
##################################### | |
def IsMSTSDefinedName( name ): | |
#if its one of the automatically animated parts | |
# TODO Include any allcaps part | |
mstsName = MSTSName( name) | |
animatedParts = ('BOGIE1','BOGIE2','WHEELS11','WHEELS12','WHEELS13','WHEELS21','WHEELS22','WHEELS23' ) | |
return animatedParts.count( mstsName.upper() ) > 0 | |
##################################### | |
def IsAnimated( nodeObject ): | |
#it has some animation defined | |
if nodeObject.animation_data != None: | |
if nodeObject.animation_data.action != None: | |
fcurves = nodeObject.animation_data.action.fcurves | |
if len(fcurves) > 0: | |
return True | |
return False | |
##################################### | |
def InLodCollections( nodeObject ): | |
global LodCollections | |
for eachLodCollection in LodCollections: | |
if nodeObject in eachLodCollection.all_objects.values(): | |
return True | |
return False | |
##################################### | |
def IsRetained( nodeObject ): | |
# return true if we should retain this level of hierarchy else collapse it down | |
if InLodCollections( nodeObject ): | |
return RetainNames or IsAnimated( nodeObject) or IsMSTSDefinedName( nodeObject.name ) | |
else: | |
return False | |
##################################### | |
def MergeChildren( nodeObject, iParent ): | |
for eachNode in nodeObject.children: | |
if IsRetained( eachNode ): | |
BuildHierarchyFrom( eachNode, iParent ) | |
else: | |
MergeChildren( eachNode, iParent ) | |
hierarchyObjects[iParent].append( eachNode ) | |
##################################### | |
def BuildHierarchyFrom( nodeObject, iParent ): | |
global hierarchy | |
global hierarchyObjects | |
index = len(hierarchy) | |
hierarchy.append( iParent ) | |
hierarchyObjects.append( [ nodeObject ] ) | |
MergeChildren( nodeObject, index ) | |
##################################### | |
# make a blender name into a legal MSTS name | |
def MSTSName( name ): | |
name = name.replace( '.','_') | |
return name | |
##################################### | |
def CreateMSTSMatrices(): | |
global ExportShape | |
global hierarchy | |
global hierarchyObjects | |
ExportShape.Matrices = [] | |
for i in range( 0, len( hierarchy ) ): | |
thisNodeObject = hierarchyObjects[i][0] | |
mstsMatrix = MSTSMatrix() | |
mstsMatrix.Label = MSTSName(thisNodeObject.name) | |
if hierarchy[i] != -1: | |
parentNodeObject = hierarchyObjects[hierarchy[i]][0] | |
blenderMatrix = ConstructMatrix( parentNodeObject, thisNodeObject ) | |
mstsMatrix.M11 = blenderMatrix[0][0] # note coordinate conversion and use of new 2.62 indexing | |
mstsMatrix.M13 = blenderMatrix[1][0] | |
mstsMatrix.M12 = blenderMatrix[2][0] | |
mstsMatrix.M31 = blenderMatrix[0][1] | |
mstsMatrix.M33 = blenderMatrix[1][1] | |
mstsMatrix.M32 = blenderMatrix[2][1] | |
mstsMatrix.M21 = blenderMatrix[0][2] | |
mstsMatrix.M23 = blenderMatrix[1][2] | |
mstsMatrix.M22 = blenderMatrix[2][2] | |
mstsMatrix.M41 = blenderMatrix[0][3] | |
mstsMatrix.M43 = blenderMatrix[1][3] | |
mstsMatrix.M42 = blenderMatrix[2][3] | |
ExportShape.Matrices.append( mstsMatrix ) | |
##################################### | |
# create an msts point for each vertex in the blender mesh | |
# return an offset into the shape's point table | |
def AddMeshVertexPoints( mesh, offsetMatrix ): | |
iPointOffset = len( ExportShape.Points ) | |
for v in mesh.vertices: | |
blenderPoint = offsetMatrix @ v.co | |
mstspoint = (blenderPoint[0],blenderPoint[2],blenderPoint[1] ) | |
ExportShape.Points.append( mstspoint ) | |
return iPointOffset | |
##################################### | |
# update the global lowerBound and upperBound vectors | |
# transform its points into world coordinates via the offsetMatrix | |
def ExtendBoundsForMesh( mesh, offsetMatrix ): | |
global UpperBound | |
global LowerBound | |
for eachVertex in mesh.vertices: | |
v = offsetMatrix @ eachVertex.co | |
if v.x > UpperBound.x: UpperBound.x = v.x | |
if v.y > UpperBound.y: UpperBound.y = v.y | |
if v.z > UpperBound.z: UpperBound.z = v.z | |
if v.x < LowerBound.x: LowerBound.x = v.x | |
if v.y < LowerBound.y: LowerBound.y = v.y | |
if v.z < LowerBound.z: LowerBound.z = v.z | |
##################################### | |
def HasGeometry( object ): | |
# make sure its not a camera, light etc that do not have geometry | |
return object.type in ['MESH'] # TODO add support for 'CURVE','SURFACE','META','FONT' | |
##################################### | |
# adds to a sub_object in this distance_level | |
# iPointOffset informs the difference between the blender vertex index and the msts vertex index | |
# normal specifies an override to the blender supplied normal, eg Normals.Face .Out .Up .Smooth etc | |
# tranform the mesh points by relativeMatrix | |
# apply the normalOverrides | |
# generate triangle lists | |
def AddTriangleToSubObject( mesh, mstsMaterial, blTriangle , windingOrder, offsetMatrix, iPointOffset ): | |
color1 = 0xFFFFFFFF # vertex colors ( when vertex color layer not present ) | |
color2 = 0xFF000000 | |
normalOverride = mstsMaterial.normalOverride | |
if normalOverride == Normals.Face: # if we didn't specify a special normal mesh property | |
if blTriangle.use_smooth: # Per face override for smooth normals | |
normalOverride = Normals.Smooth | |
subObject = mstsMaterial.subObject | |
vertexSet = subObject.VertexSets[mstsMaterial.iVertexState] | |
iPrimitive = mstsMaterial.iPrimitive | |
primitive = subObject.Primitives[iPrimitive] | |
for indexList in windingOrder: | |
mstsTriangle = [] | |
for i in indexList: | |
iblVert = blTriangle.vertices[i] | |
iLoop = blTriangle.loops[i] | |
iUVs = [] | |
for eachLayer in mstsMaterial.uv_layers: | |
bluv = eachLayer.data[iLoop].uv | |
iUV = iUVPointAdd( bluv ) | |
iUVs.append(iUV) | |
if normalOverride == Normals.Out: | |
# use tree type tangent shading ( normals radiate out from center ) | |
normal = mesh.vertices[iblVert].co | |
normal = offsetMatrix.to_3x3() @ normal | |
normal.normalize() | |
elif normalOverride == Normals.Up: | |
normal = ( 0,0,1 ) | |
elif normalOverride == Normals.Smooth: | |
# support Auto smooth | |
normal=Vector(blTriangle.split_normals[i]) | |
normal = offsetMatrix.to_3x3() @ normal | |
normal.normalize() | |
elif normalOverride == Normals.OutX: | |
# radiate out from below Y axis ( ie LPSTrack100m side vegetation ) | |
normal = offsetMatrix.translation | |
normal = Vector( (normal.x-4, 0, normal.z + 12) ) | |
normal.normalize() | |
else: | |
# flat shading uses the face normal | |
normal = blTriangle.normal | |
normal = offsetMatrix.to_3x3() @ normal | |
normal.normalize() | |
iNormal = iNormalAdd( normal ) | |
iPoint = iblVert + iPointOffset | |
if iPoint >= len( ExportShape.Points ): | |
raise Exception( 'PROGRAM ERROR: iPoint out of range' ) | |
mstsTriangle.append( iVertexAdd( iPoint, iNormal, iUVs, vertexSet, color1, color2) ) | |
# Console output, inform new draw call started | |
if len( primitive.Triangles ) == 0: | |
sequence = mstsMaterial.subObject.sequence | |
if len( mstsMaterial.iTextures) > 0: | |
texture = ExportShape.Textures[mstsMaterial.iTextures[0]] | |
filename = ExportShape.Images[texture.iImage] | |
else: | |
filename = '' | |
# DEBUG print( " SubObject ",sequence," Draw ", filename, " ", mstsMaterial.blMaterial.msts.Transparency, " ",mstsMaterial.blMaterial.msts.Lighting, " MipBias=",mstsMaterial.blMaterial.msts.MipMapLODBias ) | |
print( " Draw ", filename, " ", mstsMaterial.blMaterial.msts.Transparency, " ",mstsMaterial.blMaterial.msts.Lighting, " MipBias=",mstsMaterial.blMaterial.msts.MipMapLODBias ) | |
primitive.Triangles.append( mstsTriangle ) | |
# add a face normal ( used by MSTS for culling purposes ) | |
normal = offsetMatrix.to_3x3() @ blTriangle.normal | |
normal.normalize() | |
primitive.iNormals.append( iNormalAdd( normal ) ) | |
# build one for each material in the mesh | |
class MSTSMaterialDetail: | |
def __init__(self ): | |
self.flags = '00000400 -1 -1 000001d2 000001c4' | |
self.priority = 0 | |
self.uv_layers = [] | |
self.normalOverride = Normals.Face | |
self.subObject = None | |
self.iTextures = [] | |
self.uvops = [] | |
self.zBias = 0.0 | |
self.vertexFlags = 0 | |
self.vertexLight = LightingOptions[ 'NORMAL' ] | |
self.alphaTestMode = 0 | |
self.iShader = 0 | |
self.iLightConfig = 0 | |
self.iVertexState = 0 | |
self.iHierarchy = 0 | |
self.iPrimState = 0 | |
self.subObject = None | |
self.iPrimitive = 0 | |
self.blMaterial = None # corresponding Blender material | |
def GetMSTSMaterialDetails( distanceLevel, mesh, blMaterial, normalOverride, iHierarchy, objectName): | |
mstsMaterial = MSTSMaterialDetail() | |
mstsMaterial.blMaterial = blMaterial | |
mstsMaterial.iHierarchy = iHierarchy | |
mstsMaterial.normalOverride = normalOverride | |
if blMaterial.msts.Transparency == 'ALPHA': | |
mstsMaterial.flags = '00000400 -1 -1 000001d2 000001c4' | |
mstsMaterial.priority = 1 | |
elif blMaterial.msts.Transparency == 'ALPHA_SORT': | |
mstsMaterial.flags = '00000500 0 0 000001d2 000001c4' | |
mstsMaterial.priority = 2 | |
if blMaterial.msts.Lighting.startswith( 'SPECULAR'): | |
# clear bit 10 | |
# eg 00000500 become 00000100 and 0000400 becomes 00000000 | |
d5 = int(mstsMaterial.flags[5],16) | |
d5 &= 0b1011 | |
mstsMaterial.flags = mstsMaterial.flags[0:5]+hex(d5)[2]+mstsMaterial.flags[6:] | |
subObject = FindSubObject( distanceLevel, mstsMaterial.flags, mstsMaterial.priority, iHierarchy) | |
if subObject == None: | |
# create the required subobject | |
subObject = SubObject(distanceLevel) | |
# DEBUG print( "new subobject ", subObject.sequence ) | |
subObject.Flags = mstsMaterial.flags | |
subObject.Priority = mstsMaterial.priority | |
subObject.iHierarchy = mstsMaterial.iHierarchy | |
# every vertex_state needs a matching vertex_set | |
for eachVertexState in ExportShape.VertexStates: # note, unused vertexsets are purged at during write | |
subObject.VertexSets.append( VertexSet() ) | |
distanceLevel.SubObjects.append( subObject ) | |
mstsMaterial.subObject = subObject | |
# this could be improved to look for the uv layer in the node tree | |
# and in the future handle multiple uvs here | |
if not 'UVMap' in mesh.uv_layers: | |
raise MyException( "Missing UVMap in: " + objectName) | |
mstsMaterial.uv_layers = [ mesh.uv_layers['UVMap']] #but what about beziers generate map 'Orco' | |
# find image used by material | |
imageName = BaseColorImageFrom( blMaterial ) # may return None | |
mipMapLODBias = blMaterial.msts.MipMapLODBias | |
mstsMaterial.iTextures.append( iTextureAdd( imageName, mipMapLODBias ) ) | |
textureAddressMode = 1 # repeat | |
# textureAddressMode = 3 # extend edges | |
# textureAddressMode = 4 # clamp with border | |
# textureAddressMode = 2 # mirror | |
mstsMaterial.uvops.append( ( UVOpCopy, textureAddressMode ) ) | |
# now configure options based on the material settings | |
mstsMaterial.zBias = 0.0 | |
# Set Up Vertex Lighting | |
mstsMaterial.vertexFlags = 0 | |
mstsMaterial.vertexLight = LightingOptions[ blMaterial.msts.Lighting ] | |
if blMaterial.msts.Transparency == 'CLIP': | |
mstsMaterial.alphaTestMode = 1 | |
else: | |
mstsMaterial.alphaTestMode = 0 | |
if blMaterial.msts.Lighting == "EMISSIVE": | |
if blMaterial.msts.Transparency == 'OPAQUE': | |
mstsMaterial.iShader = iShaderAdd( 'Tex' ) | |
else: | |
mstsMaterial.iShader = iShaderAdd( 'BlendATex' ) | |
else: | |
if blMaterial.msts.Transparency == 'OPAQUE': | |
mstsMaterial.iShader = iShaderAdd( 'TexDiff' ) | |
else: | |
mstsMaterial.iShader = iShaderAdd( 'BlendATexDiff' ) | |
mstsMaterial.iLightConfig = iLightConfigAdd( mstsMaterial.uvops ) | |
mstsMaterial.iVertexState = iVertexStateAdd( mstsMaterial.vertexFlags, iHierarchy, mstsMaterial.vertexLight, mstsMaterial.iLightConfig ) | |
mstsMaterial.iPrimState = iPrimStateAdd( mstsMaterial.iVertexState, mstsMaterial.zBias, mstsMaterial.iShader, mstsMaterial.alphaTestMode, mstsMaterial.iLightConfig, mstsMaterial.iTextures ) | |
mstsMaterial.iPrimitive = iPrimitiveAdd( subObject, mstsMaterial.iPrimState ) | |
return mstsMaterial | |
##################################### | |
# add this mesh, | |
# create any needed subObjects | |
# create a prim_state referencing iHierarchy | |
# translate the mesh points by relativeMatrix | |
# apply the normalOverrides | |
# transfer all vertex points to msts points | |
# generate triangle lists | |
def AddMesh( distanceLevel, mesh, iHierarchy, offsetMatrix, normalsProperty, objectName ): | |
print( ' triangles = ', len( mesh.loop_triangles ) ) | |
# determine normal override from the object Properties | |
normalOverride = Normals.Face | |
if normalsProperty == 'UP': | |
normalOverride = Normals.Up | |
elif normalsProperty == 'OUT': | |
normalOverride = Normals.Out | |
elif normalsProperty == 'FILLET': | |
normalOverride = Normals.Fillet | |
elif normalsProperty == "OUTX": | |
normalOverride = Normals.OutX | |
# evaluate any special handling for normals | |
# Note: these may be overriden 'per face' in AddFaceToSubObject | |
if normalOverride == Normals.Fillet: | |
# preprocess normals for filleted appearance | |
# verts in a flat face will use the face normal | |
# verts in a smoothed face use the normal of the adjacent flat face | |
for tri in mesh.loop_triangles: | |
if not tri.use_smooth: | |
for iVert in tri.vertices: | |
mesh.vertices[iVert].normal = tri.normal | |
# now handle them like standard smoothed normals | |
normalOverride = Normals.Face | |
# for every vertex in the blender mesh, add a corresponding msts point | |
iPointOffset = AddMeshVertexPoints( mesh, offsetMatrix ) | |
ExtendBoundsForMesh( mesh, offsetMatrix @ hierarchyObjects[iHierarchy][0].matrix_world ) | |
# create msts materials for each mesh material | |
mstsMaterials = [] | |
for blMaterial in mesh.materials: | |
if blMaterial != None: | |
mstsMaterials.append( GetMSTSMaterialDetails( distanceLevel, mesh, blMaterial, normalOverride, iHierarchy, objectName ) ) | |
else: | |
raise MyException( "Empty Material on object: " + objectName ) | |
#if scale is negative, invert winding order of triangles | |
scale = offsetMatrix.to_scale() | |
sign = scale.x * scale.y * scale.z | |
if sign < 0: | |
windingOrder = ( (0,1,2), ) #inverted | |
else: | |
windingOrder = ( (0,2,1), ) #forward | |
# for speedup, resolve unique points only per mesh | |
global UniqueUVPoints | |
global UniqueNormals | |
UniqueUVPoints.keys.clear() | |
UniqueNormals.keys.clear() | |
for iTriangle in range( 0, len( mesh.loop_triangles ) ): | |
if iTriangle % 10 == 0: # reduce the update rate | |
UpdateProgress() | |
blTriangle = mesh.loop_triangles[iTriangle] | |
if blTriangle.material_index < len( mstsMaterials ): | |
mstsMaterial = mstsMaterials[ blTriangle.material_index ] | |
else: | |
raise MyException( "Missing Materials on object: " + objectName ) | |
# check if the subobject is full | |
subObject = mstsMaterial.subObject | |
vertexCount = 0 | |
for eachVertexSet in subObject.VertexSets: | |
vertexCount += len( eachVertexSet.Vertices ) | |
if vertexCount + 3 > MaxVerticesPerSubObject: | |
# subObject is full so start a new one | |
subObject = SplitSubObject( subObject) | |
mstsMaterial.iPrimitive = iPrimitiveAdd( subObject, mstsMaterial.iPrimState ) | |
mstsMaterial.subObject = subObject | |
# check if the primitive is full | |
iPrimitive = mstsMaterial.iPrimitive | |
primitive = subObject.Primitives[iPrimitive] | |
if len(primitive.Triangles) * 3 + 3 > MaxVerticesPerPrimitive: | |
# primitive is full so start a new one | |
iPrimitive = iPrimitiveAppend( subObject, mstsMaterial.iPrimState ) | |
primitive = subObject.Primitives[iPrimitive] | |
mstsMaterial.iPrimitive = iPrimitive | |
AddTriangleToSubObject( mesh, mstsMaterial, blTriangle, windingOrder, offsetMatrix, iPointOffset ) | |
##################################### | |
# add this collection and any child collections | |
def AddCollections( distanceLevel, collection, iHierarchy, relativeMatrix ): | |
for eachObject in collection.objects: | |
AddObject( distanceLevel, eachObject, iHierarchy, relativeMatrix @ eachObject.matrix_world ) | |
for eachChild in collection.children: | |
AddCollections( distanceLevel, eachChild, iHierarchy, relativeMatrix ) | |
##################################### | |
# add this object, | |
# transform its vertices by relativeMatrix | |
# link it to iHierarchy | |
def AddObject( distanceLevel, object, iHierarchy, relativeMatrix ): | |
print( ' ',object.name ) | |
if object.is_instancer and object.instance_collection != None: | |
# its a group instance, process each of its subobjects | |
collection = object.instance_collection | |
AddCollections( distanceLevel, collection, iHierarchy, relativeMatrix ) | |
# make sure its not a camera, light etc that do not have geometry | |
elif HasGeometry( object ): | |
# TODO handle particles | |
# TODO handle dupliverts | |
# determine normal override from the object Properties | |
normalsProperty = object.data.get( 'NORMALS', '' ) | |
# Apply Modifiers as PREVIEW | |
depsgraph = bpy.context.evaluated_depsgraph_get() | |
ob_to_convert = object.evaluated_get(depsgraph) | |
mesh = ob_to_convert.to_mesh() | |
mesh.calc_normals_split() | |
mesh.calc_loop_triangles() | |
AddMesh( distanceLevel, mesh, iHierarchy, relativeMatrix, normalsProperty, object.name ) | |
##################################### | |
# eg MAIN_1000 | |
# return ( 'MAIN',1000 ) | |
def LodDistanceFromName( collectionName ): | |
split = collectionName.rsplit( '_',1 ) | |
if len( split ) == 2: | |
if split[0] != '': | |
suffix = split[1] | |
if suffix.isnumeric( ): | |
distance = int( suffix ) | |
return distance | |
return None | |
##################################### | |
# Add the specified distanceLevel to the first LodControl | |
# Scan the hierarchy for objects that are in this collection and add them | |
# if it turns out to be empty, trim it out | |
def AppendDistanceLevel( lodCollection ): | |
global ExportShape | |
global hierarchy | |
global hierarchyObjects | |
distanceLimit = LodDistanceFromName( lodCollection.name ) | |
print( "DLEVEL "+str(distanceLimit) ) | |
# add the distance level | |
lodControl = ExportShape.LodControls[0] | |
distanceLevel = DistanceLevel( lodControl ) | |
distanceLevel.Selection = distanceLimit | |
distanceLevel.Hierarchy = hierarchy | |
lodControl.DistanceLevels.append( distanceLevel ) | |
for iHierarchy in range( 0, len( hierarchy ) ): | |
for object in hierarchyObjects[iHierarchy]: | |
if object in lodCollection.all_objects.values(): | |
nodeObject = hierarchyObjects[iHierarchy][0] | |
relativeMatrix = ConstructMatrix( nodeObject, object ) | |
AddObject( distanceLevel, object, iHierarchy, relativeMatrix ) | |
distanceLevel.SubObjects.sort( key=lambda subObject: subObject.Priority ) | |
if len( distanceLevel.SubObjects) == 0 or len( distanceLevel.SubObjects[0].Primitives ) == 0: | |
print( 'WARNING - empty distance level ',distanceLevel.Selection ) | |
del lodControl.DistanceLevels[ len( lodControl.DistanceLevels )-1 ] | |
##################################### | |
# return a vector representing the geometric center of all the geometry | |
def FindCenter( ): | |
center = ( UpperBound + LowerBound ) / 2.0 | |
return center | |
##################################### | |
# from the bounds about the specified center | |
def FindBoundingRadius( center ): | |
v = UpperBound - center | |
BoundingRadiusSquared = v.x*v.x+v.y*v.y+v.z*v.z | |
v = LowerBound - center | |
dSquared = v.x*v.x+v.y*v.y+v.z*v.z | |
if dSquared>BoundingRadiusSquared: | |
BoundingRadiusSquared = dSquared | |
return sqrt( BoundingRadiusSquared ) | |
##################################### | |
def CompactPoints(): | |
global ExportShape | |
oldPoints = ExportShape.Points | |
ExportShape.Points = [] | |
uniquePoints = UniqueArray( ExportShape.Points,3,0.0001 ) | |
conversion = [] | |
for eachPoint in oldPoints: | |
conversion.append( uniquePoints.IndexOf( eachPoint ) ) | |
for eachLODControl in ExportShape.LodControls: | |
for eachDistanceLevel in eachLODControl.DistanceLevels: | |
for eachSubObject in eachDistanceLevel.SubObjects: | |
for eachVertexSet in eachSubObject.VertexSets: | |
for eachVertex in eachVertexSet.Vertices: | |
eachVertex.iPoint = conversion[eachVertex.iPoint] | |
##################################### | |
# remove empty primitives | |
def CompactPrimitives( ): | |
emptyCount = 0 | |
for eachLODControl in ExportShape.LodControls: | |
for eachDistanceLevel in eachLODControl.DistanceLevels: | |
for eachSubObject in eachDistanceLevel.SubObjects: | |
OKPrimitives = [] | |
for eachPrimitive in eachSubObject.Primitives: | |
if len( eachPrimitive.Triangles ) > 0: | |
OKPrimitives.append( eachPrimitive ) | |
else: | |
emptyCount += 1 | |
eachSubObject.Primitives = OKPrimitives | |
# DEBUG print ( "Compacting ",emptyCount," Empty Primitives" ) | |
##################################### | |
# remove empty subobjects | |
def CompactSubObjects( ): | |
emptyCount = 0 | |
for eachLODControl in ExportShape.LodControls: | |
for eachDistanceLevel in eachLODControl.DistanceLevels: | |
OKSubObjects = [] | |
for eachSubObject in eachDistanceLevel.SubObjects: | |
if len( eachSubObject.Primitives ) > 0: | |
OKSubObjects.append( eachSubObject ) | |
else: | |
emptyCount += 1 | |
eachDistanceLevel.SubObjects = OKSubObjects | |
# DEBUG print ( "Compacting ",emptyCount," Empty SubObjects" ) | |
##################################### | |
def CreateEulerRotationController( iFC, fcurves ): | |
rotationController = RotationController() | |
#TODO this assumes all points are lined up at the same time | |
for iKey in range(0,len(fcurves[iFC].keyframe_points)): | |
key = RotationKey() | |
key.Frame = fcurves[iFC].keyframe_points[iKey].co[0] | |
x = fcurves[iFC+0].keyframe_points[iKey].co[1] | |
y = fcurves[iFC+1].keyframe_points[iKey].co[1] #Note coordinate conversion | |
z = fcurves[iFC+2].keyframe_points[iKey].co[1] | |
euler = mathutils.Euler( ( x,y,z ), 'XYZ' ) | |
quat = euler.to_quaternion() | |
key.W = quat.w | |
key.X = quat.x | |
key.Y = quat.z | |
key.Z = quat.y | |
rotationController.Keys.append( key ) | |
return rotationController | |
##################################### | |
def CreateRotationController( iFC, fcurves ): | |
rotationController = RotationController() | |
#TODO this assumes all points are lined up at the same time | |
for iKey in range(0,len(fcurves[iFC].keyframe_points)): | |
key = RotationKey() | |
key.Frame = fcurves[iFC].keyframe_points[iKey].co[0] | |
key.W = fcurves[iFC].keyframe_points[iKey].co[1] | |
key.X = fcurves[iFC+1].keyframe_points[iKey].co[1] | |
key.Y = fcurves[iFC+3].keyframe_points[iKey].co[1] #Note coordinate conversion | |
key.Z = fcurves[iFC+2].keyframe_points[iKey].co[1] | |
rotationController.Keys.append( key ) | |
return rotationController | |
##################################### | |
def CreateLinearController( iFC, fcurves ): | |
linearController = PositionController() | |
#TODO this assumes all points are lined up on the same frame | |
for iKey in range( 0, len(fcurves[iFC].keyframe_points) ): | |
key = LinearKey() | |
key.Frame = fcurves[iFC].keyframe_points[iKey].co[0] | |
key.X = fcurves[iFC].keyframe_points[iKey].co[1] | |
key.Y = fcurves[iFC+2].keyframe_points[iKey].co[1] #Note coordinate conversion | |
key.Z = fcurves[iFC+1].keyframe_points[iKey].co[1] | |
linearController.Keys.append( key ) | |
return linearController | |
##################################### | |
def CreateAnimationNode( nodeObject ): | |
animationNode = AnimationNode() | |
animationNode.Label = nodeObject.name | |
if nodeObject.animation_data != None: | |
if nodeObject.animation_data.action != None: | |
fcurves = nodeObject.animation_data.action.fcurves | |
iFC = 0 | |
while iFC < len( fcurves ): | |
if fcurves[iFC].data_path == 'rotation_quaternion': | |
animationNode.Controllers.append( CreateRotationController( iFC, fcurves ) ) | |
iFC += 4 | |
elif fcurves[iFC].data_path == 'rotation_euler': | |
animationNode.Controllers.append( CreateEulerRotationController( iFC, fcurves ) ) | |
iFC += 3 | |
elif fcurves[iFC].data_path == 'location': | |
animationNode.Controllers.append( CreateLinearController( iFC, fcurves ) ) | |
iFC += 3 | |
else: | |
print( 'Unknown controller type ',fcurves[iFC].data_path,' in ',nodeObject.name ) | |
iFC += 1 | |
return animationNode | |
##################################### | |
class SceneCenterObject: # a proxy to represent a node object at the center of the scene | |
is_instancer = False | |
type = 'PROXY' # made up for this application, not one of Blender's recognized types | |
animation_data = None | |
matrix_world = Matrix() | |
def __init__(self ): | |
self.children = [] | |
self.name = "MAIN" | |
def get( self, parameter, default): | |
return default | |
##################################### | |
def ExportShapeFile( collectionName, MSTSFilePath ): | |
global ExportShape | |
ExportShape = Shape() | |
global UniqueUVPoints | |
global UniqueNormals | |
global UniqueColors | |
global UniqueLightMaterials | |
UniqueUVPoints = UniqueArray( ExportShape.UVPoints, 3, 0.0001 ) | |
UniqueNormals = UniqueArray( ExportShape.Normals, 2, 0.001 ) | |
UniqueColors = UniqueArray( ExportShape.Colors, 3, 0.0001 ) | |
UniqueLightMaterials = UniqueArray( ExportShape.LightMaterials, 1,1 ) | |
global UpperBound | |
global LowerBound | |
UpperBound = mathutils.Vector( ( -100000,-100000,-100000 )) | |
LowerBound = mathutils.Vector( ( 100000, 100000, 100000 ) ) | |
global hierarchy # linked list ie [-1, 0,1,1,0 ] | |
global hierarchyObjects # a list of objects for each level of the hierarchy, first element in list is the retained node [ MAIN, BOGIE1, WHEELS11, WHEELS12 .. ] | |
hierarchy = [] | |
hierarchyObjects = [] | |
global LastSubObject | |
global LastMaterial | |
global LastiMatrix | |
global LastiPrimState | |
LastSubObject = None | |
LastMaterial = None | |
LastiMatrix = -1 | |
LastiPrimState = 0 | |
# For now, support a single LOD control | |
lodControl = LodControl( ExportShape ) | |
ExportShape.LodControls.append( lodControl ) | |
mainCollection = bpy.context.scene.collection.children[collectionName] | |
# get a sorted list of valid LOD Collections in MAIN | |
lodNames = [] | |
for eachChild in mainCollection.children: | |
childName = eachChild.name | |
if childName.startswith( 'MAIN_' ): | |
if LodDistanceFromName( childName ) != None: | |
lodNames.append( childName ) | |
lodNames.sort() #this sort fails if name does not have proper _xxxx suffix | |
global LodCollections | |
LodCollections = [] | |
for eachName in lodNames: | |
LodCollections.append( mainCollection.children[eachName] ) | |
if len( LodCollections ) == 0: | |
raise MyException( "No LOD collections in MAIN, eg MAIN_2000" ) | |
# add root objects to scene center | |
rootObject = SceneCenterObject( ) # make everything relative to the scene center | |
for eachObject in bpy.context.scene.objects: | |
if eachObject.parent == None: | |
rootObject.children.append( eachObject) | |
BuildHierarchyFrom( rootObject, -1 ) # create global hierarchy array | |
CreateMSTSMatrices( ) | |
# create a distance level for each one specified in MAIN | |
for eachLodCollection in LodCollections: | |
AppendDistanceLevel( eachLodCollection ) | |
# export animations | |
if len( bpy.data.actions ) > 0: | |
animation = Animation() | |
ExportShape.Animations.append(animation) | |
animation.FrameCount = bpy.context.scene.frame_end | |
animation.FrameRate = 30 | |
for eachNode in hierarchyObjects: | |
animation.AnimationNodes.append( CreateAnimationNode( eachNode[0] ) ) | |
# set up the volume sphere | |
volumeSphere = VolumeSphere() | |
# center = FindCenter( rootObject ) OR doesn't display properly with a computed center, | |
center = rootObject.matrix_world.translation | |
radius = FindBoundingRadius( center ) | |
MSTSvector = ( center.x, center.z, center.y ) | |
volumeSphere.Vector = MSTSvector | |
volumeSphere.Radius = radius * 1.1 # add some safety margin | |
ExportShape.Volumes.append( volumeSphere ) | |
print() | |
print ( "Compacting ",len( ExportShape.Points )," Points ", end='' ) | |
CompactPoints() | |
print ( " To ",len( ExportShape.Points ) ) | |
CompactPrimitives() | |
CompactSubObjects() | |
ExportShape.Write( MSTSFilePath ) | |
# Reporting | |
print ( ) | |
for lodControl in ExportShape.LodControls: | |
for distanceLevel in lodControl.DistanceLevels: | |
triangleCount = 0 | |
primitiveCount = 0 | |
for eachSubObject in distanceLevel.SubObjects: | |
for primitive in eachSubObject.Primitives: | |
if len( primitive.Triangles ) > 0: | |
primitiveCount += 1 | |
triangleCount += len(primitive.Triangles) | |
print ( "LOD: ",distanceLevel.Selection ) | |
print ( " Triangles = ", triangleCount ) | |
print ( " Draw Calls = ", primitiveCount ) | |
print ( "IMAGES:" ) | |
for eachImage in ExportShape.Images: | |
print( " ",eachImage ) | |
print() | |
return | |
''' LIBRARY FUNCTIONS | |
All code below uses the MSTS coordinate system. | |
Structure matches the MSTS .s file with the following | |
top level name substitutions: | |
VolumeSphere | |
Matrix | |
VertexState | |
Texture | |
PrimState | |
Vertex | |
Primitive | |
VertexSet | |
SubObject | |
DistanceLevel | |
LodControl | |
RotationKey | |
TCBRotationKey | |
LinearKey | |
PositionController | |
RotationController | |
AnimationNode | |
Animation | |
This structure matches the MSTS .s file with the following exceptions | |
Add vertices to vertex_sets, not subobject.vertices as in MSTS. | |
It not needed to populate the sub_object_header data. | |
Both of the above are generated from the underlying data on write. | |
''' | |
import codecs | |
import bpy | |
import math | |
from math import * | |
#################################### | |
class STFWriter: | |
#################################### | |
# | |
# Writes an MSTS structured unicode text file | |
# | |
def __init__( self, filename): | |
self.f = codecs.open(filename, 'w', encoding='utf-16') | |
def WriteLine( self, string ): | |
self.f.write( string ) | |
self.f.write( '\r\n' ) | |
def Write( self, string ): | |
self.f.write( string ) | |
def Close(self ): | |
self.f.close() | |
######################################## | |
class VolumeSphere: | |
def __init__( self ): | |
self.Vector = ( 0.0,0.0,0.0 ) | |
self.Radius = 100.0 | |
def Write( self, stf ): | |
stf.WriteLine( ' vol_sphere (' ) | |
stf.WriteLine( ' vector ( {0} {1} {2} ) {3}'.format( self.Vector[0],self.Vector[1],self.Vector[2],self.Radius)) | |
stf.WriteLine( ' )' ) | |
######################################## | |
class MSTSMatrix: | |
def __init__( self ): | |
self.Label = 'MAIN' | |
self.M11 = 1.0 | |
self.M12 = 0.0 | |
self.M13 = 0.0 | |
self.M21 = 0.0 | |
self.M22 = 1.0 | |
self.M23 = 0.0 | |
self.M31 = 0.0 | |
self.M32 = 0.0 | |
self.M33 = 1.0 | |
self.M41 = 0.0 | |
self.M42 = 0.0 | |
self.M43 = 0.0 | |
def Write( self, stf ): | |
stf.WriteLine( ' matrix {0} ( {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} )'.format( self.Label,self.M11,self.M12,self.M13,self.M21,self.M22,self.M23,self.M31,self.M32,self.M33,self.M41,self.M42,self.M43 )) | |
######################################## | |
class VertexState: | |
def __init__( self ): | |
self.Flags = 0 | |
self.iMatrix = 0 | |
self.iLightMaterial = -5 | |
self.iLightConfig = 0 | |
def Write( self, stf ): | |
stf.WriteLine( ' vtx_state ( {0:08X} {1} {2} {3} 00000002 )'.format( self.Flags, self.iMatrix, self.iLightMaterial, self.iLightConfig )) | |
######################################## | |
class Texture: | |
def __init__(self): | |
self.iImage = 0 | |
self.iFilter = 0 | |
self.MipMapLODBias = 0 # often -3 in MSTS | |
def Write( self, stf ): | |
stf.WriteLine( ' texture ( {0} {1} {2} ff000000 )'.format( self.iImage,self.iFilter,self.MipMapLODBias)) | |
######################################## | |
class UVOpCopy: # uv_op_copy | |
def __init__( self ): | |
self.TextureAddressMode = 0 #TexAddrMode | |
self.SourceUVIndex = 0 #SrcUVIdx | |
def Write( self, stf ): | |
stf.WriteLine( ' uv_op_copy ( {0} {1} )'\ | |
.format( self.TextureAddressMode, self.SourceUVIndex ) ) | |
######################################## | |
class UVOpReflectMapFull: #uv_op_reflectmap ==> :uint,TexAddrMode . | |
def __init__( self ): | |
self.TextureAddressMode = 0 #TexAddrMode | |
def Write( self, stf ): | |
stf.WriteLine( ' uv_op_reflectmapfull ( {0} )'\ | |
.format( self.TextureAddressMode ) ) | |
######################################## | |
class LightConfig: #light_model_cfg ==> :dword,flags :uv_ops . | |
def __init__( self ): | |
self.UVOps = [] | |
def Write( self, stf ): | |
stf.WriteLine( ' light_model_cfg ( 00000000' ) | |
count = len( self.UVOps ) | |
stf.WriteLine( ' uv_ops ( {0}'.format( count ) ) | |
for i in range( 0, count ): | |
self.UVOps[i].Write( stf ) | |
stf.WriteLine( ' )' ) | |
stf.WriteLine( ' )' ) | |
######################################## | |
class PrimState: | |
# eg prim_state ( 00000000 0 | |
# tex_idxs ( 1 0 ) 0 0 0 0 1 | |
# ) | |
def __init__( self ): | |
self.Label = '' | |
self.iShader = 0 | |
self.iTextures = [] | |
self.ZBias = 0.0 | |
self.iVertexState = 0 | |
self.AlphaTestMode = 1 | |
self.iLightConfig = 0 | |
self.ZBufMode = 1 | |
def Write( self,stf ): | |
stf.WriteLine( ' prim_state {0} ( 00000000 {1}'.format( self.Label,self.iShader) ) | |
stf.Write( ' tex_idxs ( {0}'.format(len(self.iTextures)) ) | |
for eachiTexture in self.iTextures: | |
stf.Write( ' {0}'.format( eachiTexture ) ) | |
stf.WriteLine(' ) {0} {1} {2} {3} {4}'.format( self.ZBias,self.iVertexState, self.AlphaTestMode, self.iLightConfig, self.ZBufMode ) ) | |
stf.WriteLine( ' )' ) | |
######################################## | |
class Vertex: | |
# eg vertex ( 00000000 0 0 ffffffff ff000000 | |
# vertex_uvs ( 1 0 ) | |
# ) | |
def __init__( self ): | |
self.iPoint = 0 | |
self.iNormal = 0 | |
self.iUVs = [] #TODO multiple UV's | |
self.Color1 = 0xffffffff | |
self.Color2 = 0xff000000 | |
def Write( self, stf ): | |
stf.WriteLine( ' vertex ( 00000000 {0} {1} {2:08X} {3:08X}'.format(self.iPoint,self.iNormal,self.Color1, self.Color2 ) ) | |
stf.Write( ' vertex_uvs ( {0}'.format( len(self.iUVs) ) ) | |
for eachiUV in self.iUVs: | |
stf.Write( ' {0}'.format(eachiUV)) | |
stf.WriteLine( ' )' ) | |
stf.WriteLine( ' )') | |
######################################## | |
class Primitive: | |
def __init__( self ): | |
self.iPrimState = 0 | |
self.Triangles = [] | |
self.iNormals = [] | |
def Write( self, stf, indexOffset ): | |
stf.WriteLine( ' indexed_trilist (' ) | |
stf.Write( ' vertex_idxs ( {0} '.format( len(self.Triangles) * 3) ) | |
linecount = 0 | |
for eachTriangle in self.Triangles: | |
for eachIndex in eachTriangle: | |
stf.Write( '{0} '.format( eachIndex + indexOffset ) ) | |
linecount += 1 | |
if linecount > 100: | |
linecount = 0 | |
stf.WriteLine( '' ) | |
stf.Write( ' ') | |
stf.WriteLine( ')') #vertex_idxs | |
stf.Write( ' normal_idxs ( {0} '.format( len(self.Triangles)) ) | |
linecount = 0 | |
for i in self.iNormals: | |
stf.Write( '{0} 3 '.format( i ) ) | |
linecount += 1 | |
if linecount > 100: | |
linecount = 0 | |
stf.WriteLine( '' ) | |
stf.Write( ' ') | |
stf.WriteLine( ')') #normal_idxs | |
stf.Write( ' flags ( {0} '.format( len(self.Triangles)) ) | |
linecount = 0 | |
for i in self.iNormals: | |
stf.Write( '00000000 ' ) | |
linecount += 1 | |
if linecount > 100: | |
linecount = 0 | |
stf.WriteLine( '' ) | |
stf.Write( ' ') | |
stf.WriteLine( ')') #flags | |
stf.WriteLine( ' )') #indexed_trilist | |
######################################## | |
class GeometryInfoPerMatrix: | |
def __init__( self, ShapeVertexStatesCount ): | |
self.PrimitivesCount = 0 | |
self.TrianglesCount = 0 | |
self.VerticesCount = 0 | |
self.VertexStatesCount = 0 | |
self.VertexStatesUsed = [] | |
for i in range(0,ShapeVertexStatesCount): | |
self.VertexStatesUsed.append( False ) | |
######################################## | |
class VertexSet: | |
def __init__( self ): | |
self.Vertices = [] | |
self.iStart = 0 # first vertex used by this set | |
self.index = {} # not exported, keyed on iPoint, list of iVertex referencing that point | |
######################################## | |
class SubObject: | |
def __init__( self, parent ): | |
self.VertexSets = [] | |
self.Primitives = [] | |
self.DistanceLevel = parent | |
self.Flags = '00000400 -1 -1 000001d2 000001c4' | |
self.Priority = 0 # sub_objects are sorted by this number, 0 comes first | |
self.iHierarchy = 0 # not exported, see HierarchyOptimization | |
self.sequence = len( self.DistanceLevel.SubObjects ) # not exported, for debugging | |
def Write( self, stf ): | |
stf.WriteLine( ' sub_object (' ) | |
self.WriteSubObjectHeader( stf ) | |
self.WriteVertices( stf ) | |
self.WriteVertexSets( stf ) | |
self.WritePrimitives( stf ) | |
stf.WriteLine( ' )') #sub_object | |
return | |
def WriteSubObjectHeader( self, stf ): | |
stf.WriteLine( ' sub_object_header ( ' + self.Flags ) | |
self.WriteSubObjectGeometryInfo( stf ) | |
self.WriteSubObjectShaders( stf ) | |
self.WriteSubObjectLightConfigs( stf ) | |
self.WriteSubObjectID( stf ) | |
stf.WriteLine( ' )') | |
def WriteSubObjectGeometryInfo( self, stf ): | |
shape = self.DistanceLevel.LodControl.Shape | |
# Set up data collection array and initialize | |
matrixInfo = [] | |
for i in range(0,len(shape.Matrices) ): | |
matrixInfo.append( GeometryInfoPerMatrix( len(shape.VertexStates) )) | |
#Scan all geometry and accumulate the statistics per matrix | |
for primitive in self.Primitives: | |
primState = shape.PrimStates[primitive.iPrimState] | |
vertexState = shape.VertexStates[primState.iVertexState] | |
iMatrix = vertexState.iMatrix | |
matrixInfo[iMatrix].PrimitivesCount += 1 | |
matrixInfo[iMatrix].TrianglesCount += len(primitive.Triangles) | |
matrixInfo[iMatrix].VerticesCount += len(primitive.Triangles) * 3 | |
matrixInfo[iMatrix].VertexStatesUsed[primState.iVertexState] = True | |
# Calculate VertexStates (txLightCmds) per matrix | |
for i in range( 0, len(matrixInfo) ): | |
matrixInfo[i].VertexStatesCount = 0 | |
for b in matrixInfo[i].VertexStatesUsed: | |
if b : matrixInfo[i].VertexStatesCount += 1 | |
# Now update the summary data | |
# Calculate FaceNormals | |
faceNormalsCount = 0 | |
for primitive in self.Primitives: | |
faceNormalsCount += len(primitive.Triangles) | |
# Calculate number of vtx_states used by primitives in this sub_objects | |
vertexStatesCount = 0 | |
for eachVertexSet in self.VertexSets: | |
if len(eachVertexSet.Vertices)>0: | |
vertexStatesCount += 1 | |
# Calculate number of vertices (VertIdxs) in all the vertex_idxs statements ( 3 x FaceNormals ) | |
verticesCount = faceNormalsCount * 3 | |
# Calculate Trilists - number of indexed_trilist statements ( in all the files I've seen, all primitives are trilists ) | |
trilistsCount = len(self.Primitives) | |
# UNKNOWN use - NodeTxLightCmds, LineListIdxs, NodeXTrilistIdxs, LineLists, PtLists , NodeXTrilists - always seems to be 0 | |
stf.WriteLine( ' geometry_info ( {0} {1} 0 {2} 0 0 {3} 0 0 0'.format(faceNormalsCount,vertexStatesCount,verticesCount,trilistsCount)) | |
# Write geometry nodes | |
# Create a new empty geometry node map and geometry nodes array | |
geometryNodeMap = [] | |
geometryNodeCount = 0 | |
for i in range( 0, len( shape.Matrices) ): | |
geometryNodeMap.append(-1) # initialize to -1's | |
if matrixInfo[i].PrimitivesCount > 0: | |
geometryNodeCount += 1 | |
# Populate the new geometry_node_map and write out the geometry_nodes | |
stf.WriteLine( ' geometry_nodes ( {0}'.format(geometryNodeCount)) | |
iGeometryNode = 0 | |
for iMatrix in range( 0, len(shape.Matrices)): | |
matrix_info = matrixInfo[iMatrix] | |
if matrix_info.PrimitivesCount > 0: | |
stf.WriteLine( ' geometry_node ( {0} 0 0 0 0'.format(matrix_info.VertexStatesCount)) | |
stf.WriteLine( ' cullable_prims ( {0} {1} {2} )'.format(matrix_info.PrimitivesCount,matrix_info.TrianglesCount,matrix_info.VerticesCount)) | |
stf.WriteLine( ' )') | |
geometryNodeMap[iMatrix] = iGeometryNode | |
iGeometryNode += 1 | |
stf.WriteLine( ' )') | |
# Write geometry node map | |
stf.Write( ' geometry_node_map ( {0} '.format(len(shape.Matrices))) | |
for iGeometryNode in geometryNodeMap: | |
stf.Write( '{0} '.format( iGeometryNode) ) | |
stf.WriteLine( ')' ) #geometry_node_map | |
stf.WriteLine( ' )') #geometry_info | |
return | |
def WriteSubObjectShaders( self, stf ): | |
shape = self.DistanceLevel.LodControl.Shape | |
subObjectShaders = set() | |
for eachPrimitive in self.Primitives: | |
primState = shape.PrimStates[eachPrimitive.iPrimState] | |
subObjectShaders |= set([primState.iShader]) | |
stf.Write( ' subobject_shaders ( {0} '.format(len(subObjectShaders) ) ) | |
for iShader in subObjectShaders: | |
stf.Write( '{0} '.format( iShader ) ) | |
stf.WriteLine( ')' ) | |
def WriteSubObjectLightConfigs( self, stf ): | |
shape = self.DistanceLevel.LodControl.Shape | |
subObjectLightConfigs = set() | |
for primitive in self.Primitives: | |
primState = shape.PrimStates[primitive.iPrimState] | |
subObjectLightConfigs |= set([primState.iLightConfig]) | |
stf.Write( ' subobject_light_cfgs ( {0} '.format(len(subObjectLightConfigs) ) ) | |
for iLightConfig in subObjectLightConfigs: | |
stf.Write( '{0} '.format( iLightConfig ) ) | |
stf.Write( ')' ) | |
def WriteSubObjectID( self, stf ): | |
stf.WriteLine( ' 0' ) # [:uint,SubObjID] | |
def WriteVertices( self, stf ): # and set start location for vertex_set's | |
# count the vertices | |
count = 0 | |
for vertexSet in self.VertexSets: | |
count += len( vertexSet.Vertices ) | |
stf.WriteLine( ' vertices ( {0}'.format(count)) | |
# write out the vertices | |
iStart = 0 | |
for vertexSet in self.VertexSets: | |
vertexSet.iStart = iStart | |
for vertex in vertexSet.Vertices: | |
vertex.Write( stf ) | |
iStart += len( vertexSet.Vertices ) | |
stf.WriteLine( ' )') # vertices | |
def WriteVertexSets( self, stf ): | |
# count the vertex sets | |
count = 0 | |
for vertexSet in self.VertexSets: | |
if len( vertexSet.Vertices ) > 0: | |
count += 1 | |
stf.WriteLine( ' vertex_sets ( {0}'.format(count) ) | |
# write out the vertex sets | |
for i in range( 0, len( self.VertexSets ) ): | |
vertexSet = self.VertexSets[i] | |
if len( vertexSet.Vertices ) > 0: | |
stf.WriteLine( ' vertex_set ( {0} {1} {2} )'.format(i,vertexSet.iStart,len(vertexSet.Vertices))) | |
stf.WriteLine( ' )' ) #vertex_sets | |
return | |
def WritePrimitives( self,stf ): | |
shape = self.DistanceLevel.LodControl.Shape | |
print ("Writing primitives") | |
# determine count | |
count = 0 | |
iPrimState = -1 | |
for eachPrimitive in self.Primitives: | |
count += 1 | |
if eachPrimitive.iPrimState != iPrimState: | |
iPrimState = eachPrimitive.iPrimState | |
count += 1 | |
stf.WriteLine( ' primitives ( {0}'.format(count)) | |
# and write out the primitives | |
iPrimState = -1 | |
for eachPrimitive in self.Primitives: | |
if eachPrimitive.iPrimState != iPrimState: | |
iPrimState = eachPrimitive.iPrimState | |
stf.WriteLine( ' prim_state_idx ( {0} )'.format( iPrimState ) ) | |
primState = shape.PrimStates[iPrimState] | |
vertexSet = self.VertexSets[ primState.iVertexState] | |
eachPrimitive.Write( stf, vertexSet.iStart ) | |
stf.WriteLine( ' )' ) # primitives | |
####################################### | |
class DistanceLevel: | |
#hierarchy | |
#dlevel_selection | |
def __init__(self,parent): | |
self.LodControl = parent | |
self.SubObjects = [] | |
self.Selection = 0 # maximum distance this distance level is visible | |
self.Hierarchy = [] | |
def Write( self, stf ): | |
stf.WriteLine( ' distance_level (' ) | |
print ("Writing distance level") | |
self.WriteHeader( stf ) | |
count = len( self.SubObjects ) | |
stf.WriteLine( ' sub_objects ( {0}'.format(count)) | |
for i in range( 0,count): | |
self.SubObjects[i].Write(stf) | |
stf.WriteLine( ' )') | |
stf.WriteLine( ' )') | |
def WriteHeader( self, stf ): | |
stf.WriteLine( ' distance_level_header (' ) | |
stf.WriteLine( ' dlevel_selection ( {0} )'.format( self.Selection ) ) | |
count = len( self.Hierarchy ) | |
stf.Write( ' hierarchy ( {0} '.format( count ) ) | |
for i in range( 0,count ): | |
stf.Write( '{0} '.format( self.Hierarchy[i] ) ) | |
stf.WriteLine( ')') | |
stf.WriteLine( ' )' ) | |
######################################## | |
class LodControl: | |
def __init__( self, parent ): | |
self.Shape = parent | |
self.DistanceLevels = [] | |
def Write( self, stf ): | |
stf.WriteLine( ' lod_control (' ) | |
stf.WriteLine( ' distance_levels_header ( 0 )' ) | |
count = len( self.DistanceLevels ) | |
stf.WriteLine( ' distance_levels ( {0}'.format(count)) | |
for i in range(0,count): | |
self.DistanceLevels[i].Write(stf) | |
stf.WriteLine( ' )') | |
stf.WriteLine( ' )') | |
######################################## | |
class RotationKey: #key | |
def __init__(self ): | |
self.Frame = 0 | |
self.X = 0 | |
self.Y = 0 | |
self.Z = 0 | |
self.W = 0 | |
def Write( self, stf ): | |
stf.WriteLine( ' slerp_rot ( {0} {1} {2} {3} {4} )'\ | |
.format( int(self.Frame), round(self.X,8), round(self.Y,8), round(self.Z,8), round(self.W,8) )) # note frame must be an int for ffeditc_unicode to compress it properly | |
######################################## | |
class TCBRotationKey: #key | |
def __init__(self ): | |
self.Frame = 0 | |
self.X = 0 | |
self.Y = 0 | |
self.Z = 0 | |
self.W = 0 | |
self.Tension = 0 | |
self.Continuity = 0 | |
self.Bias = 0 | |
self.In = 0 | |
self.Out = 0 | |
def Write( self, stf ): | |
stf.WriteLine( ' tcb_key ( {0} {1} {2} {3} {4} {5} {6} {7} {8} {9} )'\ | |
.format( int(self.Frame), self.X, self.Y, self.Z, self.W, self.Tension, self.Continuity, self.Bias, self.In, self.Out )) | |
######################################## | |
class LinearKey: # key | |
def __init__(self ): | |
self.Frame = 0 | |
self.X = 0 | |
self.Y = 0 | |
self.Z = 0 | |
def Write( self, stf ): | |
stf.WriteLine( ' linear_key ( {0} {1} {2} {3} )'\ | |
.format( int(self.Frame), round(self.X,8), round(self.Y,8),round( self.Z,8) )) | |
######################################## | |
class PositionController: # controller | |
def __init__( self ): | |
self.Keys = [] | |
def Write( self, stf ): | |
stf.WriteLine(' linear_pos ( {0}'.format(len(self.Keys))) | |
for eachKey in self.Keys: | |
eachKey.Write( stf ) | |
stf.WriteLine(' )') | |
######################################## | |
class RotationController: # controller | |
def __init__( self ): | |
self.Keys = [] | |
def Write( self, stf ): | |
stf.WriteLine(' tcb_rot ( {0}'.format(len(self.Keys))) | |
for eachKey in self.Keys: | |
eachKey.Write( stf ) | |
stf.WriteLine(' )') | |
######################################## | |
class AnimationNode: | |
def __init__( self ): | |
self.Label = None | |
self.Controllers = [] | |
def Write( self, stf ): | |
stf.WriteLine( ' anim_node {0} ('.format( self.Label ) ) | |
stf.WriteLine( ' controllers ( {0}'.format( len(self.Controllers) ) ) | |
for eachController in self.Controllers: | |
eachController.Write( stf ) | |
stf.WriteLine( ' )' ) | |
stf.WriteLine( ' )' ) | |
######################################## | |
class Animation: | |
def __init__( self ): | |
self.FrameCount = 0 | |
self.FrameRate = 30 | |
self.AnimationNodes = [] | |
def Write( self, stf ): | |
stf.WriteLine( ' animation ( {0} {1}'.format( int(self.FrameCount), self.FrameRate) ) | |
stf.WriteLine( ' anim_nodes ( {0}'.format( len(self.AnimationNodes) ) ) | |
for eachAnimationNode in self.AnimationNodes: | |
eachAnimationNode.Write( stf ) | |
stf.WriteLine( ' )') | |
stf.WriteLine( ' )' ) | |
######################################## | |
class Shape: | |
def __init__( self ): | |
self.Volumes = [] | |
self.Shaders = [] | |
self.Filters = [] | |
self.Points = [] | |
self.UVPoints = [] | |
self.Normals = [] | |
self.Matrices = [] | |
self.Images = [] | |
self.Textures = [] | |
self.Colors = [] | |
self.LightMaterials = [] | |
self.LightConfigs = [] | |
self.VertexStates = [] | |
self.PrimStates = [] | |
self.LodControls = [] | |
self.Animations = [] | |
def Write( self, filepath ): | |
stf = STFWriter( filepath ) | |
stf.WriteLine( 'SIMISA@@@@@@@@@@JINX0s1t______\r\n' ) | |
stf.WriteLine( 'shape (' ) | |
stf.WriteLine( ' shape_header ( 00000000 00000000 )' ) | |
self.WriteVolumes( stf ) | |
self.WriteShaders( stf ) | |
self.WriteFilters( stf ) | |
self.WritePoints( stf ) | |
self.WriteUVPoints( stf ) | |
self.WriteNormals( stf ) | |
self.WriteSortVectors( stf ) | |
self.WriteColours( stf ) | |
self.WriteMatrices( stf ) | |
self.WriteImages(stf) | |
self.WriteTextures(stf ) | |
self.WriteLightMaterials(stf) | |
self.WriteLightConfigs( stf ) | |
self.WriteVertexStates(stf) | |
self.WritePrimStates(stf) | |
self.WriteLodControls(stf) | |
self.WriteAnimations(stf) | |
stf.WriteLine( ')' ) | |
stf.Close() | |
def WriteVolumes( self, stf ): | |
print ("Writing volumes") | |
count = len( self.Volumes ) | |
stf.WriteLine( ' volumes ( {0}'.format(count) ) | |
for i in range( 0,count): | |
self.Volumes[i].Write( stf ) | |
stf.WriteLine( ' )' ) | |
def WriteShaders( self, stf ): | |
print ("Writing shader names") | |
count = len( self.Shaders ) | |
stf.WriteLine( ' shader_names ( {0}'.format( count )) | |
for i in range( 0, count ): | |
stf.WriteLine( ' named_shader ( {0} )'.format( self.Shaders[i] )) | |
stf.WriteLine( ' )') | |
def WriteFilters( self, stf ): | |
print ("Writing texture filter names") | |
count = len( self.Filters ) | |
stf.WriteLine( ' texture_filter_names ( {0}'.format(count)) | |
for i in range( 0, count ): | |
stf.WriteLine( ' named_filter_mode ( {0} )'.format( self.Filters[i])) | |
stf.WriteLine( ' )') | |
def WritePoints( self, stf ): | |
print ("Writing points") | |
count = len( self.Points ) | |
stf.WriteLine( ' points ( {0}'.format(count) ) | |
for i in range( 0, count ): | |
p = self.Points[i] | |
stf.WriteLine( ' point ( {0} {1} {2} )'.format(round(p[0],6),round(p[1],6),round(p[2],6) ) ) | |
stf.WriteLine( ' )' ) | |
def WriteUVPoints( self, stf ): | |
print ("Writing uv points") | |
count = len( self.UVPoints ) | |
stf.WriteLine( ' uv_points ( {0}'.format(count) ) | |
for i in range( 0, count ): | |
p = self.UVPoints[i] | |
stf.WriteLine( ' uv_point ( {0} {1} )'.format(round(p[0],6),round(p[1],6)) ) | |
stf.WriteLine( ' )' ) | |
def WriteNormals( self, stf ): | |
print ("Writing normals") | |
count = len( self.Normals ) | |
stf.WriteLine( ' normals ( {0}'.format(count) ) | |
for i in range( 0, count ): | |
p = self.Normals[i] | |
stf.WriteLine( ' vector ( {0} {1} {2} )'.format(round(p[0],6),round(p[1],6),round(p[2],6) ) ) | |
stf.WriteLine( ' )' ) | |
def WriteSortVectors( self, stf ): | |
stf.WriteLine( ' sort_vectors ( 1' ) | |
stf.WriteLine( ' vector ( 0 0 0 )' ) | |
stf.WriteLine( ' )' ) | |
def WriteMatrices( self, stf ): | |
print ("Writing matrices") | |
count = len( self.Matrices ) | |
stf.WriteLine( ' matrices ( {0}'.format( count ) ) | |
for i in range( 0,count): | |
self.Matrices[i].Write( stf ) | |
stf.WriteLine( ' )' ) | |
def WriteImages( self, stf ): | |
print ("Writing image names") | |
count = len( self.Images ) | |
stf.WriteLine( ' images ( {0}'.format(count)) | |
for i in range(0,count): | |
imageFileName = self.Images[i] | |
if imageFileName.count( ' ' ) == 0: #only use quotes when spaces exist in name for SVIEW compatibility | |
stf.WriteLine( ' image ( {0} )'.format( imageFileName ) ) | |
else: | |
stf.WriteLine( ' image ( "{0}" )'.format( imageFileName ) ) | |
stf.WriteLine( ' )' ) | |
def WriteTextures( self, stf ): | |
print ("Writing textures") | |
count = len( self.Textures ) | |
stf.WriteLine( ' textures ( {0}'.format(count)) | |
for i in range(0,count): | |
self.Textures[i].Write(stf) | |
stf.WriteLine( ' )' ) | |
def WriteColours( self, stf ): | |
count = len( self.Colors ) | |
stf.WriteLine( ' colours ( {0}'.format( count) ) | |
for i in range( 0, count ): | |
c = self.Colors[i] # a r g b | |
stf.WriteLine( ' colour ( {0} {1} {2} {3} )'.format( c[0],c[1],c[2],c[3] ) ) | |
stf.WriteLine( ' )' ) | |
def WriteLightMaterials( self, stf ): | |
count = len( self.LightMaterials ) | |
stf.WriteLine( ' light_materials ( {0}'.format( count) ) | |
for i in range( 0, count ): | |
m = self.LightMaterials[i] | |
stf.WriteLine( ' light_material ( 00000000 {0} {1} {2} {3} {4} )'.format( m[0],m[1],m[2],m[3],m[4] ) ) | |
stf.WriteLine( ' )' ) | |
def WriteLightConfigs( self, stf ): | |
print( "Writing light configs" ) | |
count = len( self.LightConfigs ) | |
stf.WriteLine( ' light_model_cfgs ( {0}'.format(count)) | |
for i in range( 0,count ): | |
self.LightConfigs[i].Write(stf) | |
stf.WriteLine( ' )') | |
def WriteVertexStates( self, stf ): | |
print ("Writing vertex states") | |
count = len( self.VertexStates ) | |
stf.WriteLine( ' vtx_states ( {0}'.format(count)) | |
for i in range( 0,count): | |
self.VertexStates[i].Write( stf ) | |
stf.WriteLine( ' )') | |
def WritePrimStates( self, stf ): | |
print ("Writing prim states") | |
count = len( self.PrimStates ) | |
stf.WriteLine( ' prim_states ( {0}'.format(count)) | |
for i in range( 0,count ): | |
self.PrimStates[i].Write(stf) | |
stf.WriteLine( ' )') | |
def WriteLodControls(self, stf ): | |
count = len( self.LodControls ) | |
stf.WriteLine( ' lod_controls ( {0}'.format( count ) ) | |
for i in range(0,count): | |
self.LodControls[i].Write(stf) | |
stf.WriteLine( ' )') | |
def WriteAnimations(self, stf ): | |
count = len( self.Animations ) | |
if count > 0: # ShapeViewer crashes if you have animations( 0 ) | |
stf.WriteLine( ' animations ( {0}'.format( count ) ) | |
for i in range(0,count): | |
self.Animations[i].Write(stf) | |
stf.WriteLine( ' )') | |
''' | |
************************* end library ******************************* | |
''' | |
if __name__ == "__main__": | |
unregister() | |
register() | |
# print( "START" ) | |
# ExportShapeFile( 'MAIN', r'C:\MSTS\GLOBAL\SHAPES\LPSTrack100m.s' ) #r'c:\users\wayne\desktop\out.s' ) # | |
# print( "DONE" ) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment