Init Repo

This commit is contained in:
indigo 2023-09-24 15:04:35 +08:00
commit 4c8bf1460f
8 changed files with 757 additions and 0 deletions

143
LDD.py Normal file
View File

@ -0,0 +1,143 @@
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import sys
import os
import logging
import subprocess as sp
import reader.lxf_loader as lxf_loader
import reader.ldd_command as ldd_command
reload(lxf_loader)
reload(ldd_command)
logger = logging.getLogger('LDD')
kFileVersion = ''
kExtension = '.lxf'
kPluginName = 'Lego Digital Designer Reader for Maya '
kFilterName = '*.lxf'
kTranslatorName = 'LXF File'
class LXFReader(OpenMayaMPx.MPxFileTranslator):
def __init__(self):
OpenMayaMPx.MPxFileTranslator.__init__(self)
def haveReadMethod(self):
return True
def haveWriteMethod(self):
return False
def canBeOpened(self):
return True
def translatorName(self):
return kTranslatorName
def defaultExtension(self):
return kExtension
def filter(self):
return kFilterName
def identifyFile(self, fileName, buf, size):
if 'lxf' in fileName.name().lower():
logger.info('File is LDD lxf %s' % fileName.name())
return OpenMayaMPx.MPxFileTranslator.kIsMyFileType
return OpenMayaMPx.MPxFileTranslator.kNotMyFileType
def reader(self, fileName, options, mode):
logger.info('reader')
try:
loader = lxf_loader.LXFLoader()
loader.read(fileName.rawFullName())
except:
sys.stderr.write("Failed reader : %s" % kPluginName)
(typ, value, traceback) = sys.exc_info()
sys.excepthook(typ, value, traceback)
raise
def lxf_reader_creator():
return OpenMayaMPx.asMPxPtr(LXFReader())
def check_ldd_install():
program_dir = os.getenv('ProgramFiles(x86)')
ldd_install_dir = os.path.join(program_dir, 'LEGO Company', 'LEGO Digital Designer')
exec_file = os.path.normpath(os.path.join(ldd_install_dir, 'LDD.exe'))
if not os.path.exists(exec_file):
return False
cmd = r'powershell.exe [System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"%s\").FileVersion' % exec_file
ldd_version = ''
p = sp.Popen(cmd, shell=True, stdout=sp.PIPE)
for li in p.stdout:
ldd_version = li.strip()
asset_file = os.path.join(ldd_install_dir, 'Assets.lif')
if not os.path.exists(asset_file):
logger.warning('LEGO Digital Designer Assets.lif not found.')
return False
logger.debug('LEGO Digital Designer %s installed.' % ldd_version)
return True
def init_ldd_asset_map():
""" Pre generate asset.lif data index
for reading geometry data
"""
def initializePlugin(mobject):
if os.getenv('OS') not in ['Windows_NT']:
sys.stderr.write('LDD.py only support window platform.')
return
if not check_ldd_install():
sys.stderr.write('LEGO Digital Designer not install.')
return
mplugin = OpenMayaMPx.MFnPlugin(mobject, "CGCG", "1.0", "Any")
try:
mplugin.registerFileTranslator(kTranslatorName,
None,
lxf_reader_creator,
None,
None,
False)
logger.debug('Register LDD File Translater :: %s' % kTranslatorName)
except:
sys.stderr.write("Failed to register translator: %s\n" % kPluginName)
raise
try:
mplugin.registerCommand(ldd_command.kPluginCmdName,
ldd_command.ldd_command_creater,
ldd_command.ldd_command_syntaxCreator)
logger.debug('Register LDD Command :: %s' % ldd_command.kPluginCmdName)
except:
sys.stderr.write('Failed to register command: ' + ldd_command.kPluginCmdName)
raise
def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterFileTranslator(kTranslatorName)
logger.debug('Deregister LDD File Translator :: %s' % kTranslatorName)
except:
sys.stderr.write("Failed to deregister translator: %s\n" % kPluginName)
raise
try:
mplugin.deregisterCommand(ldd_command.kPluginCmdName)
except:
sys.stderr.write('Failed to deregister command: ' + ldd_command.kPluginCmdName)
raise

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# Lego Digital Designer Reader for Maya
This plugin is for tranlate geometry from Lego Digital Designer, extract grometry from lego part number and material color. The base idea is from git project ***[LIF Extractor](https://github.com/JrMasterModelBuilder/LIF-Extractor)***
### Requirement
* Lego Digital Designer 4.3+
* Maya 2018 +
### Limitation
Current some geometry with hole will translate incorrect result and uvs.

0
reader/__init__.py Normal file
View File

BIN
reader/__init__.pyc Normal file

Binary file not shown.

169
reader/asset_reader.py Normal file
View File

@ -0,0 +1,169 @@
"""
LIF Extractor - LEGO Digital Designer LIF extractor.
Copyright (C) 2012 JrMasterModelBuilder
You accept full responsibility for how you use this program.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
LDD_INSTALL_PATH = r'C:\Program Files (x86)\LEGO Company\LEGO Digital Designer'
LDD_ASSET_PATH = os.path.join(LDD_INSTALL_PATH, 'Assets.lif')
fileData = ''
def asset_extract(design_ids=None, material_only=False, debug=True):
global fileData
def uint32(offset):
if bytesAre == "str":
return (ord(fileData[offset]) * 16777216) + (ord(fileData[offset + 1]) * 65536) + (
ord(fileData[offset + 2]) * 256) + ord(fileData[offset + 3])
else:
return (fileData[offset] * 16777216) + (fileData[offset + 1] * 65536) + (fileData[offset + 2] * 256) + \
fileData[offset + 3]
def uint16(offset):
if bytesAre == "str":
return (ord(fileData[offset]) * 256) + ord(fileData[offset + 1])
else:
return (fileData[offset] * 256) + fileData[offset + 1]
def recurse(prefix, offset):
if prefix == "":
offset += 36
else:
folderList.extend([prefix])
offset += 4
for i in range(uint32(offset)):
offset += 4
entryType = uint16(offset) # 1 = directory, 2 = file
print 'xxx', entryType, offset
offset += 6
# Build the name.
entryName = os.sep
# Check both integer and byte for different versions of Python.
while fileData[offset + 1] != 0 and fileData[offset + 1] != b"\x00":
print '---', offset, fileData[offset + 1]
if bytesAre == "str":
entryName += fileData[offset + 1]
else:
entryName += chr(fileData[offset + 1])
offset += 2
offset += 6
if entryType == 1:
# Recurse through the director, and update the offset.
packedFilesOffset[0] += 20
offset = recurse(prefix + entryName, offset)
elif entryType == 2:
# File offset.
packedFilesOffset[0] += 20
fileOffset = packedFilesOffset[0]
# File size.
fileSize = uint32(offset) - 20
offset += 24
packedFilesOffset[0] += fileSize
fileList.extend([[prefix + entryName, fileOffset, fileSize]])
# Return the offset at the end of this run to update parent runnings.
return offset
if debug:
logger.debug('PROCESSING: %s' % LDD_ASSET_PATH)
# Open the file if valid.
try:
with open(LDD_ASSET_PATH, "rb") as f:
fileData = f.read()
except IOError as e:
logger.error('ERROR: Failed to read file.')
return
if len(fileData) < 4 or fileData[0:4] != b"LIFF":
logger.error('ERROR: Not a LIF file.')
return
if debug:
logger.debug('EXTRACTING: Asset.lif: Please wait.')
# Recurse through, creating a list of all the files.
packedFilesOffset = [84] # Array for non-global function-modifiable variable.
folderList = []
fileList = []
# Check if this version of Python treats bytes as int or str
bytesAre = type(b'a'[0]).__name__
# Recuse through the archive.
recurse("", (uint32(72) + 64))
for i in fileList:
if 'db.lif' in i[0]:
fileData = fileData[i[1]:i[1] + i[2]]
break
if debug:
logger.debug('PROCESSING: db.lif')
if len(fileData) < 4 or fileData[0:4] != b"LIFF":
logger.error("\tERROR: Not a LIF file.")
return
if debug:
logger.debug('EXTRACTING db.lif: Please wait.')
# Recurse through, creating a list of all the files.
packedFilesOffset = [84] # Array for non-global function-modifiable variable.
folderList = []
fileList = []
# Check if this version of Python treats bytes as int or str
bytesAre = type(b'a'[0]).__name__
# Recuse through the archive.
recurse("", (uint32(72) + 64))
if material_only:
for i in fileList:
material_file = r'\Materials.xml'
if i[0] in [material_file]:
m_data = fileData[i[1]:i[1] + i[2]]
return m_data
return None
if design_ids:
g_data_map = {}
g_file_list = [r'\Primitives\LOD0\%s.g' % d for d in design_ids]
for i in fileList:
if i[0] not in g_file_list:
continue
g_data = fileData[i[1]:i[1] + i[2]]
design_id = design_ids[g_file_list.index(i[0])]
g_data_map[design_id] = g_data
return g_data_map
else:
design_id_list = []
for i in fileList:
if r'\Primitives\LOD0' not in i[0]:
continue
design_id = os.path.splitext(os.path.basename(i[0]))[0]
design_id_list.append(design_id)
return design_id_list
if __name__ == '__main__':
asset_extract('99818', material_only=True)

106
reader/ldd_command.py Normal file
View File

@ -0,0 +1,106 @@
import asset_reader
from lxf_loader import LXFLoader
import maya.api.OpenMaya as OpenMaya
import maya.OpenMaya as om
import maya.OpenMayaMPx as OpenMayaMPx
import logging
import maya.cmds as mc
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def load_part(design_ids, debug=False):
# design_ids = ['99251']
part_data_map = asset_reader.asset_extract(design_ids=design_ids, debug=debug)
mesh_created = []
if not part_data_map:
return mesh_created
for design_id in design_ids:
if debug:
logger.debug('Load LDD part %s' % design_id)
status, v_list, n_list, f_ids = LXFLoader.get_geometry(design_id, part_data_map, debug=debug)
vertices = [OpenMaya.MPoint(v_list[x], v_list[x + 1], v_list[x + 2]) for x in
range(0, len(v_list), 3)]
polygon_counts = [3 for i in range(0, len(f_ids), 3)]
fn_mesh = OpenMaya.MFnMesh()
fn_mesh.create(vertices, polygon_counts, f_ids)
mesh_parent = fn_mesh.parent(0)
mesh_transform = OpenMaya.MFnTransform(mesh_parent)
mesh_transform.setName('lego_part_%s_mesh' % design_id)
force_element = 'initialShadingGroup'
mc.sets(mesh_transform.fullPathName(), e=True, forceElement=force_element)
if debug:
logger.debug('Load LDD part %s finished.' % design_id)
mesh_created.append(mesh_transform.fullPathName())
return mesh_created
def part_exists(design_id, debug=False):
design_id_list = asset_reader.asset_extract(debug=debug)
if design_id in design_id_list:
return True
return False
def get_part_list(debug=False):
return asset_reader.asset_extract(debug=debug)
kPluginCmdName = 'ldd'
kImportFlag = '-ip'
kImportFlagLong = '-importPart'
kExistsFlag = '-ex'
kExistsFlagLong = '-exists'
kListFlag = '-lp'
kListFlagLong = '-listParts'
kDebugFlag = '-d'
kDebugFlagLong = '-debug'
class LddCommand(OpenMayaMPx.MPxCommand):
def __init__(self):
"""Constructor. """
OpenMayaMPx.MPxCommand.__init__(self)
def doIt(self, args):
return self.parseArguments(args)
def parseArguments(self, args):
arg_data = om.MArgParser(self.syntax(), args)
debug = False
if arg_data.isFlagSet(kDebugFlagLong):
debug = True
if arg_data.isFlagSet(kImportFlagLong):
design_id_str = arg_data.flagArgumentString(kImportFlagLong, 0)
design_ids = [design_id_str]
if ',' in design_id_str:
design_ids = design_id_str.split(',')
self.setResult(load_part(design_ids, debug=debug))
if arg_data.isFlagSet(kExistsFlagLong):
design_id = arg_data.flagArgumentString(kExistsFlagLong, 0)
self.setResult(part_exists(design_id, debug=debug))
if arg_data.isFlagSet(kListFlagLong):
self.setResult(sorted(get_part_list(debug=debug)))
def ldd_command_syntaxCreator():
syntax = om.MSyntax()
syntax.addFlag(kImportFlag, kImportFlagLong, om.MSyntax.kString)
syntax.addFlag(kExistsFlag, kExistsFlagLong, om.MSyntax.kString)
syntax.addFlag(kListFlag, kListFlagLong, om.MSyntax.kNoArg)
syntax.addFlag(kDebugFlag, kDebugFlagLong, om.MSyntax.kNoArg)
return syntax
def ldd_command_creater():
return OpenMayaMPx.asMPxPtr(LddCommand())

94
reader/lif_reader.py Normal file
View File

@ -0,0 +1,94 @@
import os
import struct
LDD_INSTALL_PATH = r'C:\Program Files (x86)\LEGO Company\LEGO Digital Designer'
LDD_ASSET_PATH = os.path.join(LDD_INSTALL_PATH, 'Assets.lif')
def read():
def uint32(fd, offset):
fd.seek(offset)
buff = fd.read(4)
if bytesAre == 'str':
return (ord(buff[0]) * 16777216) + \
(ord(buff[1]) * 65536) + \
(ord(buff[2]) * 256) + \
ord(buff[3])
else:
return (buff[0] * 16777216) + \
(buff[1] * 65536) + \
(buff[2] * 256) + \
buff[3]
def uint16(fd, offset):
fd.seek(offset)
buff = fd.read(2)
if bytesAre == "str":
return (ord(buff[0]) * 256) + \
ord(buff[1])
else:
return (fd.read(1) * 256) + \
fd.read(1)
def recurse(fd, prefix, offset):
if prefix == "":
offset += 36
else:
folder_list.extend([prefix])
offset += 4
for i in range(uint32(fd, offset)):
offset += 4
entryType = uint16(fd, offset) # 1 = directory, 2 = file
print 'xxx', entryType, offset
offset += 6
# Build the name.offset
entryName = os.sep
while True:
fd.seek(offset)
read_chr = ''.join(struct.unpack('<s', fd.read(1)))
print '---', offset, read_chr
if not (read_chr != 0 and read_chr != b'\x00'):
break
if bytesAre == 'str':
entryName += read_chr
else:
entryName += chr(read_chr)
offset += 2
offset += 6
if entryType == 1:
packed_files_offset[0] += 20
offset = recurse(fd, prefix + entryName, offset)
elif entryType == 2:
packed_files_offset[0] += 20
fileOffset = packed_files_offset[0]
# File size.
fileSize = uint32(fd, offset) - 20
offset += 24
packed_files_offset[0] += fileSize
file_list.extend([[prefix + entryName, fileOffset, fileSize]])
return offset
bytesAre = type(b'a'[0]).__name__
packed_files_offset = [84] # Array for non-global function-modifiable variable.
folder_list = []
file_list = []
with open(LDD_ASSET_PATH, "rb") as fd:
file_format = ''.join(struct.unpack('<ssss', fd.read(4)))
if file_format != 'LIFF':
return
print file_format
recurse(fd, "", (uint32(fd, 72) + 64))
for i in file_list:
print i
# read()

235
reader/lxf_loader.py Normal file
View File

@ -0,0 +1,235 @@
import struct
import os
import logging
import asset_reader
import cStringIO
import maya.api.OpenMaya as OpenMaya
import maya.cmds as mc
import maya.mel as mm
import xml.etree.ElementTree as ET
from zipfile import ZipFile
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class LXFLoader(object):
def __init__(self):
self.material_index = {}
def get_material(self):
self.material_index = {}
material_data = asset_reader.asset_extract(material_only=True)
if not material_data:
logger.warning('Can\'t extract material info')
return False
root = ET.fromstring(material_data)
gMainProgressBar = mm.eval('$tmp = $gMainProgressBar')
mc.progressBar(gMainProgressBar,
edit=True,
beginProgress=True,
isInterruptable=True,
status='Reading LDD materials ...',
maxValue=len(root))
logger.info('Get material data ...')
for materail in root:
if mc.progressBar(gMainProgressBar, query=True, isCancelled=True):
mc.progressBar(gMainProgressBar, edit=True, endProgress=True)
return False
mat_id = materail.get('MatID')
mat_r = materail.get('Red')
mat_g = materail.get('Green')
mat_b = materail.get('Blue')
mat_a = materail.get('Alpha')
if mat_id not in self.material_index.keys():
self.material_index[mat_id] = {'r': int(mat_r),
'g': int(mat_g),
'b': int(mat_b),
'a': int(mat_a)}
mc.progressBar(gMainProgressBar, edit=True, step=1)
mc.progressBar(gMainProgressBar, edit=True, endProgress=True)
return True
@staticmethod
def get_lxfml_name(zipf):
name_list = zipf.namelist()
for name in name_list:
if os.path.splitext(name)[-1] in ['.LXFML']:
return name
return ''
@staticmethod
def get_geometry(design_id, part_data_map, output=None, debug=True):
def convert_to_obj(v_list, n_list, f_ids, output_file):
with open(output_file, 'w') as f:
f.write("# OBJ file\n")
for vid in range(0, len(v_list), 3):
f.write('v %s %s %s\n' % (v_list[vid], v_list[vid + 1], v_list[vid + 2]))
f.write('\n')
for nid in range(0, len(n_list), 3):
f.write('vn %s %s %s\n' % (n_list[nid], n_list[nid + 1], n_list[nid + 2]))
f.write('\n')
for fid in range(0, len(f_ids), 3):
f.write('f %s %s %s\n' % (f_ids[fid] + 1, f_ids[fid + 1] + 1, f_ids[fid + 2] + 1))
if design_id not in part_data_map.keys():
return False, [], [], []
vertex = []
normal = []
index = []
g_buffer = part_data_map[design_id]
f = cStringIO.StringIO(g_buffer)
# Read header part
struct.unpack('<i', f.read(4))
vertex_count = struct.unpack('<i', f.read(4))[0]
index_count = struct.unpack('<i', f.read(4))[0]
struct.unpack('<i', f.read(4))
if debug:
logger.debug('-' * 60)
logger.debug('\tRead Part %s : vertex count : %s' % (design_id, vertex_count))
logger.debug('\tRead Part %s : index count : %s' % (design_id, index_count))
logger.debug('-' * 60)
# Read content part
for i in range(vertex_count):
vertex.append(struct.unpack('<f', f.read(4))[0])
vertex.append(struct.unpack('<f', f.read(4))[0])
vertex.append(struct.unpack('<f', f.read(4))[0])
for i in range(vertex_count):
normal.append(struct.unpack('<f', f.read(4))[0])
normal.append(struct.unpack('<f', f.read(4))[0])
normal.append(struct.unpack('<f', f.read(4))[0])
for i in range(index_count):
index.append(struct.unpack('<i', f.read(4))[0])
f.close()
if output:
convert_to_obj(vertex, normal, index, output)
return True, vertex, normal, index
return True, vertex, normal, index
def read(self, file_path_name):
has_material = self.get_material()
part_cache = {}
zipf = ZipFile(file_path_name)
lxfml = self.get_lxfml_name(zipf)
lxfml_content = zipf.open(lxfml).read()
root = ET.fromstring(lxfml_content)
part_data_list = []
design_ids = []
for element in root:
if element.tag == 'Bricks':
for brick in element:
for part in brick:
design_id = part.get('designID')
mat_id = part.get('materials')
for bone in part:
m = [float(x) for x in bone.get('transformation').split(',')]
part_data = dict(designID=design_id, materials=mat_id, transformation=m)
part_data_list.append(part_data)
if design_id not in design_ids:
design_ids.append(design_id)
part_data_map = asset_reader.asset_extract(design_ids=design_ids)
if not part_data_map:
return
gMainProgressBar = mm.eval('$tmp = $gMainProgressBar')
mc.progressBar(gMainProgressBar,
edit=True,
beginProgress=True,
isInterruptable=True,
status='Importing LDD models ...',
maxValue=len(part_data_list))
for part_data in part_data_list:
if mc.progressBar(gMainProgressBar, query=True, isCancelled=True):
logger.warning('Import LDD models interrupt...')
break
design_id = part_data['designID']
mat_id = part_data['materials']
m = part_data['transformation']
if design_id not in part_cache.keys():
logger.debug(
'[%s/%s] Get new part %s' % (part_data_list.index(part_data), len(part_data_list), design_id))
status, v_list, n_list, f_ids = self.get_geometry(design_id, part_data_map)
part_cache[design_id] = [v_list, f_ids]
else:
logger.debug(
'[%s/%s] Get cached part of %s' % (part_data_list.index(part_data), len(part_data_list), design_id))
v_list = part_cache[design_id][0]
f_ids = part_cache[design_id][1]
vertices = [OpenMaya.MPoint(v_list[x], v_list[x + 1], v_list[x + 2]) for x in
range(0, len(v_list), 3)]
polygon_counts = [3 for i in range(0, len(f_ids), 3)]
fn_mesh = OpenMaya.MFnMesh()
fn_mesh.create(vertices, polygon_counts, f_ids)
mesh_parent = fn_mesh.parent(0)
mesh_transform = OpenMaya.MFnTransform(mesh_parent)
mesh_transform.setName('lego_part_%s_mesh' % design_id)
m_matrix = OpenMaya.MMatrix([m[0], m[1], m[2], 0,
m[3], m[4], m[5], 0,
m[6], m[7], m[8], 0,
m[9], m[10], m[11], 1])
transform_matrix = OpenMaya.MTransformationMatrix(m_matrix)
mesh_transform.setTransformation(transform_matrix)
force_element = 'initialShadingGroup'
if has_material:
if mat_id in self.material_index.keys():
shd_name = 'shd_%s' % mat_id
sg_name = '%sSG' % shd_name
if not mc.objExists(shd_name):
color_data = self.material_index[mat_id]
r = float(color_data.get('r')) / 255.0
g = float(color_data.get('g')) / 255.0
b = float(color_data.get('b')) / 255.0
a = float(color_data.get('a')) / 255.0
transparency = 1.0 - a
mc.shadingNode('blinn', asShader=True, name=shd_name)
mc.setAttr('%s.color' % shd_name, r, g, b, type='double3')
mc.setAttr('%s.transparency' % shd_name,
transparency, transparency, transparency,
type='double3')
mc.sets(renderable=True, noSurfaceShader=True, empty=True, name=sg_name)
mc.connectAttr('%s.outColor' % shd_name, '%s.surfaceShader' % sg_name, f=True)
force_element = sg_name
mc.sets(mesh_transform.fullPathName(), e=True, forceElement=force_element)
mc.progressBar(gMainProgressBar, edit=True, step=1)
mc.progressBar(gMainProgressBar, edit=True, endProgress=True)