I’m releasing my latest Blender plugin to the public!
Last time I promised I'd be back soon with the next one — and here we are.
This time, you don’t have to deal with any annoying conversions like before.
Now, you can import directly from Joymax file formats like .bms, .bsk, .bmt, and .ddj into Blender v4.5.1 LTS.
(Tested on Windows 10, Blender 4.5.1 LTS — Steam version)
It's still not as perfect as I want it to be, but I'm getting closer to it.
Plugin Installation:
• First, download Blender (v4.5.1 is recommended). You can get it from the official site or the Steam Store.
• Once installed, Open Blender.
• Click on Edit > Preferences...
• In the new window, go to the Add-ons section.
• In the top-right corner, click Install from Disk...
•Browse to the Python file you downloaded [UPDATE]bms_bsk_bmt_ddj_import_only_final_HU.py and select it.
• Finally, make sure the plugin is enabled by checking the box next to its name ("Import-Export: Silkroad JMX Importer (Community Final)").
Required: Installing the Pillow Library
To correctly load .ddj textures, Blender needs an external Python library called "Pillow". You only need to perform this installation **once** per Blender installation.
1. In Blender, switch to the **"Scripting"** workspace in the top menu bar. You will find the **Python Console** in one of the lower windows. 🐍
2. Copy the full command below and paste it into the console:
Code:
import ensurepip; ensurepip._bootstrap(); import pip; pip.main(['install', 'Pillow'])
4. To complete the installation, **restart Blender!** 🔄
Installation guide in pictures:
I'M GOING TO ADD MORE SOON
How to Get Game Files:
If you just want to take a quick look, I've included the D12 CH BOW files next to the plugin.
Download any Silkroad Online client — I tested this on vSRO-based clients.
Open the .pk2 files (like Media.pk2, Data.pk2, etc.) using a PK2 extractor.
(I’ve attached PK2 Tools in this thread’s downloads section as well.)
Search for the asset you want. In this tutorial, I’m using the D12 CH Bow, which includes:
• bow_12.bms (mesh)
• bow_12.bsk (skeleton)
• bow_12.bmt (material)
• bow_12.ddj (texture)
Use the search function inside the PK2 Extractor to find all `bow_12.*` files quickly.
Example file paths from the extracted Data.pk2:
• \Data\prim\mesh\item\china\weapon\bow_12.bms
• \Data\prim\skel\item\china\weapon\bow_12.bsk
• \Data\prim\mtrl\item\china\weapon\bow_12.bmt
• \Data\prim\mtrl\item\china\weapon\bow_12.ddj
Copy all of them to your desktop or any folder you prefer.
Open the .pk2 files (like Media.pk2, Data.pk2, etc.) using a PK2 extractor.
(I’ve attached PK2 Tools in this thread’s downloads section as well.)
Search for the asset you want. In this tutorial, I’m using the D12 CH Bow, which includes:
• bow_12.bms (mesh)
• bow_12.bsk (skeleton)
• bow_12.bmt (material)
• bow_12.ddj (texture)
Use the search function inside the PK2 Extractor to find all `bow_12.*` files quickly.
Example file paths from the extracted Data.pk2:
• \Data\prim\mesh\item\china\weapon\bow_12.bms
• \Data\prim\skel\item\china\weapon\bow_12.bsk
• \Data\prim\mtrl\item\china\weapon\bow_12.bmt
• \Data\prim\mtrl\item\china\weapon\bow_12.ddj
Copy all of them to your desktop or any folder you prefer.
Time to Import Your First Asset:
Start a new project in Blender and press the **N** key to open the side toolbar.
Go to the Silkroad tab to begin.
In the plugin panel:
• 1st field: .BMS File (mesh)
• 2nd field: .BSK File (skeleton)
• 3rd field: .BMT File (material)
• 4th field: .DDJ (texture)
NOTE: This plugin now automatically finds all necessary files if they are in the same folder and share the same name as the selected one. If you select a file with the wrong field, it will give you a UI and console notification to help you choose the correct file type.
If you want to use the asset in another game engine or 3D software, just go to `File > Export > FBX (.fbx)`.
Go to the Silkroad tab to begin.
In the plugin panel:
• 1st field: .BMS File (mesh)
• 2nd field: .BSK File (skeleton)
• 3rd field: .BMT File (material)
• 4th field: .DDJ (texture)
NOTE: This plugin now automatically finds all necessary files if they are in the same folder and share the same name as the selected one. If you select a file with the wrong field, it will give you a UI and console notification to help you choose the correct file type.
If you want to use the asset in another game engine or 3D software, just go to `File > Export > FBX (.fbx)`.
A Note on Exporting and Future Updates
• EXPORT: This plugin was created to import models from SRO into Blender, mainly so you can export them as .fbx for use in modern game engines like Unreal Engine 5. Unfortunately, exporting back to Joymax formats is NOT supported.
Implementing a custom exporter is incredibly complex, and since it wasn't a goal for my own projects, I decided not to invest the time and energy into it.
Sorry!
• UPDATE: The word “final” in the plugin name doesn’t mean it’s perfect — it just means this version is feature-complete for its intended purpose, and I won’t be releasing further updates for it.
Also, sorry for the Hungarian comments in the code! 😅
I tried writing everything in English, but since it’s not my native language, I kept making mistakes and losing focus.
Implementing a custom exporter is incredibly complex, and since it wasn't a goal for my own projects, I decided not to invest the time and energy into it.
Sorry!
• UPDATE: The word “final” in the plugin name doesn’t mean it’s perfect — it just means this version is feature-complete for its intended purpose, and I won’t be releasing further updates for it.
Also, sorry for the Hungarian comments in the code! 😅
I tried writing everything in English, but since it’s not my native language, I kept making mistakes and losing focus.
That’s all for now.
I’m already working on the next plugin, which will be able to import even more complex assets.
Be patient, everyone — I’m doing my best!
Everything here is free, just like the Thanks button.
[NEW_UPDATE]IMPORT ONLY: bms_bsk_bmt_ddj_import_only_final_HU (Blender 4.5.1 LTS)
bl_info = {
"name": "Silkroad JMX Importer (Community Final)",
"author": "szabo176",
"version": (4, 5, 3), # Hibajavítás
"blender": (4, 1, 0),
"location": "View3D Sidebar > Silkroad",
"description": "Imports BMS models with BMT/DDJ support, and BSK skeletons with automatic rigging.",
"category": "Import-Export"
}
import bpy
import os
import bmesh
import struct
import tempfile
import io
import math
try:
from PIL import Image
PILLOW_OK = True
except ImportError:
PILLOW_OK = False
from bpy.props import StringProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Matrix, Quaternion, Vector
# --- Segédfüggvények ---
def read_int(f): return int.from_bytes(f.read(4), 'little')
def read_short(f): return int.from_bytes(f.read(2), 'little')
def read_byte(f): return int.from_bytes(f.read(1), 'little')
def read_float(f): return struct.unpack('<f', f.read(4))[0]
def read_color4(f): return struct.unpack('<4f', f.read(16))
def read_str(f):
str_len = read_int(f)
if str_len <= 0: return ""
str_bytes = f.read(str_len)
try: return str_bytes.decode("cp949")
except UnicodeDecodeError: return str_bytes.decode("utf-8", errors='ignore')
# --- Képfeldolgozás ---
def convert_ddj_to_png(ddj_path):
if not PILLOW_OK: raise ImportError("A Pillow (PIL) könyvtár szükséges a DDJ konverzióhoz.")
try:
with open(ddj_path, 'rb') as f:
f.seek(20); dds_data = f.read()
image = Image.open(io.BytesIO(dds_data))
temp_png_path = os.path.join(tempfile.gettempdir(), os.path.basename(ddj_path) + ".png")
image.save(temp_png_path, 'PNG')
return temp_png_path
except Exception as e:
print(f"DDJ -> PNG konverziós hiba: {e}"); return None
# --- BMT Parser ---
def read_bmt_file(bmt_filepath):
if not bmt_filepath or not os.path.exists(bmt_filepath): return {}
materials = {}
print(f"[LOG] BMT fájl megnyitása: {os.path.basename(bmt_filepath)}")
try:
with open(bmt_filepath, 'rb') as f:
if f.read(7) != b"JMXVBMT":
print(" [HIBA] Nem érvényes BMT szignatúra."); return {}
f.read(5)
count = read_int(f)
print(f" -> Anyagok száma a BMT-ben: {count}")
for _ in range(count):
mat_name = read_str(f)
props = {
'name': mat_name, 'diffuse': read_color4(f), 'ambient': read_color4(f),
'specular': read_color4(f), 'emissive': read_color4(f),
'shininess': read_float(f), 'flags': read_int(f),
'texture': None, 'normal_map': None
}
if props['flags'] & 0x100:
props['texture'] = read_str(f)
f.read(4); f.read(1); f.read(1); f.read(1)
if props['flags'] & 0x2000:
props['normal_map'] = read_str(f)
f.read(4)
materials[mat_name] = props
print(f" -> '{mat_name}' anyag beolvasva.")
except Exception as e:
print(f"Hiba a BMT fájl olvasása közben: {e}")
return materials
# --- BSK Parser ---
def read_bsk_file(bsk_filepath):
if not bsk_filepath or not os.path.exists(bsk_filepath): return None
bones_data = []
print(f"[LOG] BSK fájl megnyitása: {os.path.basename(bsk_filepath)}")
try:
with open(bsk_filepath, 'rb') as f:
if f.read(7) != b"JMXVBSK":
print(" [HIBA] Nem érvényes BSK szignatúra."); return None
f.read(5)
bone_count = read_int(f)
print(f" [LOG] {bone_count} csont beolvasása...")
for i in range(bone_count):
f.read(1)
bone_name = read_str(f)
parent_name = read_str(f)
f.read(16 + 12)
rot_abs = struct.unpack('<4f', f.read(16))
pos_abs = struct.unpack('<3f', f.read(12))
f.seek(16 + 12, 1)
child_count = read_int(f)
for _ in range(child_count): read_str(f)
bones_data.append({"name": bone_name, "parent": parent_name, "pos": pos_abs, "rot": rot_abs})
print(f" -> Csont beolvasva ({i+1}/{bone_count}): {bone_name} (Szülő: {parent_name or 'Nincs'})")
except Exception as e:
print(f"Hiba a BSK fájl olvasása közben: {e}"); return None
return bones_data
# --- Armature Építő ---
def create_armature(name, bones_data, context):
print("[LOG] Armature létrehozása...")
armature_data = bpy.data.armatures.new(name=name + "_Armature")
armature_obj = bpy.data.objects.new(armature_data.name, armature_data)
context.collection.objects.link(armature_obj)
context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
blender_bones = {}
for bone_info in bones_data:
bl_bone = armature_data.edit_bones.new(name=bone_info['name'])
blender_bones[bone_info['name']] = bl_bone
for bone_info in bones_data:
bl_bone = blender_bones[bone_info['name']]
if bone_info['parent'] and bone_info['parent'] in blender_bones:
bl_bone.parent = blender_bones[bone_info['parent']]
sro_quat = Quaternion((bone_info['rot'][3], bone_info['rot'][0], bone_info['rot'][1], bone_info['rot'][2]))
sro_pos = Vector(bone_info['pos'])
bl_bone.matrix = Matrix.Translation(sro_pos) @ sro_quat.to_matrix().to_4x4()
bpy.ops.object.mode_set(mode='OBJECT')
print(f" [LOG] Armature létrehozva: {armature_obj.name}")
return armature_obj
# --- UI és Operátorok ---
class SROProperties(PropertyGroup):
def _autofind_worker(self, active_path):
if getattr(self, "is_autofinding", False) or not active_path or not os.path.exists(active_path): return
try:
setattr(self, "is_autofinding", True)
directory = os.path.dirname(active_path)
base_name = os.path.splitext(os.path.basename(active_path))[0]
bmt_path = os.path.join(directory, base_name + '.bmt')
if not self.import_bmt_filepath and os.path.exists(bmt_path): self.import_bmt_filepath = bmt_path
bsk_path = os.path.join(directory, base_name + '.bsk')
if not self.import_bsk_filepath and os.path.exists(bsk_path): self.import_bsk_filepath = bsk_path
bms_path = os.path.join(directory, base_name + '.bms')
if not self.import_bms_filepath and os.path.exists(bms_path): self.import_bms_filepath = bms_path
ddj_path = os.path.join(directory, base_name + '.ddj')
if not self.import_texture_filepath and os.path.exists(ddj_path): self.import_texture_filepath = ddj_path
finally:
setattr(self, "is_autofinding", False)
def bms_update(self, context): self._autofind_worker(self.import_bms_filepath)
def bmt_update(self, context): self._autofind_worker(self.import_bmt_filepath)
def bsk_update(self, context): self._autofind_worker(self.import_bsk_filepath)
def texture_update(self, context): self._autofind_worker(self.import_texture_filepath )
import_bms_filepath: StringProperty(name=".BMS File", subtype='FILE_PATH', description="Select a .bms model file", update=bms_update)
import_bmt_filepath: StringProperty(name=".BMT File", subtype='FILE_PATH', description="Select a .bmt material file", update=bmt_update)
import_bsk_filepath: StringProperty(name=".BSK File", subtype='FILE_PATH', description="Select a .bsk skeleton file", update=bsk_update)
import_texture_filepath: StringProperty(name="Texture File", subtype='FILE_PATH', description="Default texture if BMT is not used or texture is missing (.ddj only)", update=texture_update)
class SRO_OT_ImportUI(Operator):
bl_idname = "silkroad.import_bms"
bl_label = "Import Model"
def execute(self, context):
print(f"\n--- Új Importálási Folyamat Indul (v{bl_info['version'][0]}.{bl_info['version'][1]}.{bl_info['version'][2]}) ---")
if not PILLOW_OK: self.report({'ERROR'}, "Pillow library (PIL) is not installed. DDJ conversion will fail."); return {'CANCELLED'}
props = context.scene.sro_props
bms_path, bmt_path, bsk_path, texture_path_default = props.import_bms_filepath, props.import_bmt_filepath, props.import_bsk_filepath, props.import_texture_filepath
if not bms_path or not os.path.exists(bms_path):
print("[HIBA] Nincs .bms fájl kiválasztva, vagy a fájl nem létezik.")
self.report({'ERROR'}, "BMS file not selected or does not exist."); return {'CANCELLED'}
if not bms_path.lower().endswith('.bms'):
print(f"[HIBA] Érvénytelen BMS fájl: '{os.path.basename(bms_path)}'. Kérlek, .bms kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid file for BMS. Please select a .bms file."); return {'CANCELLED'}
if bmt_path and not bmt_path.lower().endswith('.bmt'):
print(f"[HIBA] Érvénytelen BMT fájl: '{os.path.basename(bmt_path)}'. Kérlek, .bmt kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid file for BMT. Please select a .bmt file."); return {'CANCELLED'}
if bsk_path and not bsk_path.lower().endswith('.bsk'):
print(f"[HIBA] Érvénytelen BSK fájl: '{os.path.basename(bsk_path)}'. Kérlek, .bsk kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid file for BSK. Please select a .bsk file."); return {'CANCELLED'}
if texture_path_default and not texture_path_default.lower().endswith('.ddj'):
print(f"[HIBA] Érvénytelen textúra fájl: '{os.path.basename(texture_path_default)}'. Kérlek, .ddj kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid default texture. Please select a .ddj file."); return {'CANCELLED'}
try:
bmt_data = read_bmt_file(bmt_path)
bones_data = read_bsk_file(bsk_path)
verts, normals, uvs, faces, bones, weights = [], [], [], [], [], []
mesh_name_from_bms, mat_name_from_bms = "", ""
print(f"[LOG] BMS fájl megnyitása: {os.path.basename(bms_path)}")
with open(bms_path, 'rb') as f:
if f.read(7) != b"JMXVBMS": raise ValueError("Not a valid BMS file signature.")
f.read(5)
header_offsets = struct.unpack('<10I', f.read(40))
header = {"vertex_offset": header_offsets[0], "skin_offset": header_offsets[1], "face_offset": header_offsets[2]}
print(f" -> Header beolvasva. Vertex offset: {header['vertex_offset']}, Skin offset: {header['skin_offset']}, Face offset: {header['face_offset']}")
f.read(8); vertex_flag = read_int(f); f.read(4)
mesh_name_from_bms, mat_name_from_bms = read_str(f), read_str(f)
f.read(4)
f.seek(header["vertex_offset"]); vcount = read_int(f)
print(f" [LOG] {vcount} vertex beolvasása...")
for _ in range(vcount):
verts.append(struct.unpack('<3f', f.read(12)))
normals.append(struct.unpack('<3f', f.read(12)))
uvs.append(struct.unpack('<2f', f.read(8)))
if vertex_flag & 0x400: f.read(8)
if vertex_flag & 0x800: f.read(36)
f.read(12)
print(f" -> Vertex adatok beolvasva: {len(verts)} pozíció, {len(normals)} normál, {len(uvs)} UV.")
f.seek(header["face_offset"]); fcount = read_int(f)
print(f" [LOG] {fcount} lap (face) beolvasása...")
faces = [tuple(reversed(tuple(read_short(f) for _ in range(3)))) for _ in range(fcount)]
print(f" -> Lap adatok beolvasva: {len(faces)} lap.")
if header["skin_offset"] > 0:
f.seek(header["skin_offset"]); bcount = read_int(f)
if bcount > 0:
print(f" [LOG] {bcount} csont (bone) és súlyozás beolvasása...")
bones = [read_str(f) for _ in range(bcount)]
print(f" -> Mesh-hez tartozó csontok: {', '.join(bones)}")
for _ in range(vcount):
bi1, bw1, bi2, bw2 = read_byte(f), read_short(f), read_byte(f), read_short(f)
total = bw1 + bw2 if (bw1 + bw2) > 0 else 1.0
weights.append((bi1, bw1/total, bi2, bw2/total))
print(f" -> Súlyozási adatok beolvasva {len(weights)} vertexhez.")
base_name = mesh_name_from_bms or os.path.splitext(os.path.basename(bms_path))[0]
container_obj = bpy.data.objects.new(base_name, None)
context.collection.objects.link(container_obj)
print("[LOG] Blender objektum létrehozása...")
mesh = bpy.data.meshes.new(base_name + '_Mesh')
mesh_obj = bpy.data.objects.new(base_name + "_Mesh", mesh)
mesh_obj.parent = container_obj
context.collection.objects.link(mesh_obj)
mesh.from_pydata(verts, [], faces); mesh.update()
if normals:
mesh.normals_split_custom_set_from_vertices(normal s)
mesh.shade_smooth()
print(" [LOG] Custom normals sikeresen alkalmazva.")
bm = bmesh.new(); bm.from_mesh(mesh)
uv_layer = bm.loops.layers.uv.new("UVMap")
for face in bm.faces:
for loop in face.loops:
loop[uv_layer].uv = (uvs[loop.vert.index][0], 1.0 - uvs[loop.vert.index][1])
bm.to_mesh(mesh); bm.free()
print(" [LOG] UV map sikeresen létrehozva.")
armature_obj = None
if bones_data:
armature_obj = create_armature(base_name, bones_data, context)
armature_obj.parent = container_obj
# JAVÍTÁS: A mesh objektumot tesszük újra aktívvá az anyagbeállítás előtt
context.view_layer.objects.active = mesh_obj
if bones and weights:
print(f" [LOG] Vertex csoportok létrehozása és súlyozás...")
for b_name in bones: mesh_obj.vertex_groups.new(name=b_name)
for i, w in enumerate(weights):
bi1, bw1, bi2, bw2 = w
if bw1 > 0.001 and bi1 < len(bones): mesh_obj.vertex_groups[bones[bi1]].add([i], bw1, 'REPLACE')
if bw2 > 0.001 and bi2 < len(bones): mesh_obj.vertex_groups[bones[bi2]].add([i], bw2, 'ADD')
if armature_obj:
modifier = mesh_obj.modifiers.new(name='Armature', type='ARMATURE')
modifier.object = armature_obj
print(f" [LOG] Súlyozás sikeresen alkalmazva.")
print("[LOG] Anyag létrehozása...")
mat_props = bmt_data.get(mat_name_from_bms)
final_mat_name = (mat_props.get('name') if mat_props else mat_name_from_bms) or "Material"
mat = bpy.data.materials.new(name=final_mat_name)
mat.use_nodes = True
mesh_obj.data.materials.append(mat)
nodes, links = mat.node_tree.nodes, mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
if not bsdf:
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
bms_dir = os.path.dirname(bms_path)
bmt_dir = os.path.dirname(bmt_path) if bmt_path else ""
def find_texture(texture_name, default_path):
if not texture_name: return default_path
candidate = os.path.join(bmt_dir, texture_name)
if os.path.exists(candidate): return candidate
candidate_bms = os.path.join(bms_dir, texture_name)
if os.path.exists(candidate_bms): return candidate_bms
print(f" [FIGYELEM] A '{texture_name}' textúra nem található a BMT/BMS mappában.")
return default_path
def process_texture(texture_path, texture_type="Diffuse"):
if not texture_path or not os.path.exists(texture_path) or not texture_path.lower().endswith('.ddj'):
return None
print(f" [LOG] {texture_type} textúra feldolgozása: {os.path.basename(texture_path)}")
try:
png_path = convert_ddj_to_png(texture_path)
print(f" -> Sikeresen konvertálva ide: {png_path}")
return png_path
except Exception as e:
print(f" [HIBA] A(z) {texture_type} textúra konvertálása sikertelen: {e}")
return None
diffuse_tex_name = mat_props.get('texture') if mat_props else None
diffuse_path = find_texture(diffuse_tex_name, texture_path_default)
png_path_diffuse = process_texture(diffuse_path, "Diffuse")
if png_path_diffuse:
tex_node = nodes.new("ShaderNodeTexImage")
tex_node.image = bpy.data.images.load(png_path_diffuse)
print(f" -> Kép betöltve a Blenderbe: {os.path.basename(png_path_diffuse)}")
mat.node_tree.nodes.active = tex_node
links.new(bsdf.inputs['Base Color'], tex_node.outputs['Color'])
if mat_props and mat_props['flags'] & 0x200:
mat.blend_method = 'BLEND'
if hasattr(mat, "eevee"):
mat.eevee.shadow_method = 'HASHED'
print(f" -> Diffuse textúra sikeresen hozzárendelve a shaderhez.")
normal_tex_name = mat_props.get('normal_map') if mat_props else None
if normal_tex_name:
normal_path = find_texture(normal_tex_name, "")
png_path_normal = process_texture(normal_path, "Normal Map")
if png_path_normal:
norm_tex_node = nodes.new("ShaderNodeTexImage")
norm_tex_node.image = bpy.data.images.load(png_path_normal)
norm_tex_node.image.colorspace_settings.name = 'Non-Color'
norm_map_node = nodes.new("ShaderNodeNormalMap")
links.new(norm_tex_node.outputs['Color'], norm_map_node.inputs['Color'])
links.new(norm_map_node.outputs['Normal'], bsdf.inputs['Normal'])
print(f" -> Normal Map sikeresen hozzárendelve a shaderhez.")
if mat_props:
bsdf.inputs['Base Color'].default_value = mat_props['diffuse']
bsdf.inputs['Specular IOR Level'].default_value = mat_props['specular'][0]
bsdf.inputs['Roughness'].default_value = max(0.0, min(1.0, 1.0 - (mat_props['shininess'] / 128.0)))
print("[LOG] Importálás befejezése, objektum elforgatása...")
container_obj.rotation_euler[0] = math.radians(-90)
self.report({'INFO'}, f"Successfully imported: {container_obj.name}")
print("--- Importálási Folyamat Befejeződött ---")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Import failed: {e}")
import traceback; traceback.print_exc()
return {'CANCELLED'}
class VIEW3D_PT_sro_panel(Panel):
bl_label="Silkroad Tools"; bl_idname="VIEW3D_PT_silkroad_panel"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category='Silkroad'
def draw(self, context):
props=context.scene.sro_props; box=self.layout.box()
box.label(text="Model & Skeleton Import", icon='IMPORT')
box.prop(props, "import_bms_filepath")
box.prop(props, "import_bsk_filepath")
box.prop(props, "import_bmt_filepath")
box.prop(props, "import_texture_filepath", text=".DDJ File")
box.operator(SRO_OT_ImportUI.bl_idname)
# --- Regisztráció ---
classes = (SROProperties, SRO_OT_ImportUI, VIEW3D_PT_sro_panel)
def register():
if not PILLOW_OK: print("WARNING: 'Pillow' Python library not installed. DDJ textures cannot be loaded.")
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.sro_props = PointerProperty(type=SROProperties)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.sro_props
if __name__ == "__main__":
register()
"name": "Silkroad JMX Importer (Community Final)",
"author": "szabo176",
"version": (4, 5, 3), # Hibajavítás
"blender": (4, 1, 0),
"location": "View3D Sidebar > Silkroad",
"description": "Imports BMS models with BMT/DDJ support, and BSK skeletons with automatic rigging.",
"category": "Import-Export"
}
import bpy
import os
import bmesh
import struct
import tempfile
import io
import math
try:
from PIL import Image
PILLOW_OK = True
except ImportError:
PILLOW_OK = False
from bpy.props import StringProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Matrix, Quaternion, Vector
# --- Segédfüggvények ---
def read_int(f): return int.from_bytes(f.read(4), 'little')
def read_short(f): return int.from_bytes(f.read(2), 'little')
def read_byte(f): return int.from_bytes(f.read(1), 'little')
def read_float(f): return struct.unpack('<f', f.read(4))[0]
def read_color4(f): return struct.unpack('<4f', f.read(16))
def read_str(f):
str_len = read_int(f)
if str_len <= 0: return ""
str_bytes = f.read(str_len)
try: return str_bytes.decode("cp949")
except UnicodeDecodeError: return str_bytes.decode("utf-8", errors='ignore')
# --- Képfeldolgozás ---
def convert_ddj_to_png(ddj_path):
if not PILLOW_OK: raise ImportError("A Pillow (PIL) könyvtár szükséges a DDJ konverzióhoz.")
try:
with open(ddj_path, 'rb') as f:
f.seek(20); dds_data = f.read()
image = Image.open(io.BytesIO(dds_data))
temp_png_path = os.path.join(tempfile.gettempdir(), os.path.basename(ddj_path) + ".png")
image.save(temp_png_path, 'PNG')
return temp_png_path
except Exception as e:
print(f"DDJ -> PNG konverziós hiba: {e}"); return None
# --- BMT Parser ---
def read_bmt_file(bmt_filepath):
if not bmt_filepath or not os.path.exists(bmt_filepath): return {}
materials = {}
print(f"[LOG] BMT fájl megnyitása: {os.path.basename(bmt_filepath)}")
try:
with open(bmt_filepath, 'rb') as f:
if f.read(7) != b"JMXVBMT":
print(" [HIBA] Nem érvényes BMT szignatúra."); return {}
f.read(5)
count = read_int(f)
print(f" -> Anyagok száma a BMT-ben: {count}")
for _ in range(count):
mat_name = read_str(f)
props = {
'name': mat_name, 'diffuse': read_color4(f), 'ambient': read_color4(f),
'specular': read_color4(f), 'emissive': read_color4(f),
'shininess': read_float(f), 'flags': read_int(f),
'texture': None, 'normal_map': None
}
if props['flags'] & 0x100:
props['texture'] = read_str(f)
f.read(4); f.read(1); f.read(1); f.read(1)
if props['flags'] & 0x2000:
props['normal_map'] = read_str(f)
f.read(4)
materials[mat_name] = props
print(f" -> '{mat_name}' anyag beolvasva.")
except Exception as e:
print(f"Hiba a BMT fájl olvasása közben: {e}")
return materials
# --- BSK Parser ---
def read_bsk_file(bsk_filepath):
if not bsk_filepath or not os.path.exists(bsk_filepath): return None
bones_data = []
print(f"[LOG] BSK fájl megnyitása: {os.path.basename(bsk_filepath)}")
try:
with open(bsk_filepath, 'rb') as f:
if f.read(7) != b"JMXVBSK":
print(" [HIBA] Nem érvényes BSK szignatúra."); return None
f.read(5)
bone_count = read_int(f)
print(f" [LOG] {bone_count} csont beolvasása...")
for i in range(bone_count):
f.read(1)
bone_name = read_str(f)
parent_name = read_str(f)
f.read(16 + 12)
rot_abs = struct.unpack('<4f', f.read(16))
pos_abs = struct.unpack('<3f', f.read(12))
f.seek(16 + 12, 1)
child_count = read_int(f)
for _ in range(child_count): read_str(f)
bones_data.append({"name": bone_name, "parent": parent_name, "pos": pos_abs, "rot": rot_abs})
print(f" -> Csont beolvasva ({i+1}/{bone_count}): {bone_name} (Szülő: {parent_name or 'Nincs'})")
except Exception as e:
print(f"Hiba a BSK fájl olvasása közben: {e}"); return None
return bones_data
# --- Armature Építő ---
def create_armature(name, bones_data, context):
print("[LOG] Armature létrehozása...")
armature_data = bpy.data.armatures.new(name=name + "_Armature")
armature_obj = bpy.data.objects.new(armature_data.name, armature_data)
context.collection.objects.link(armature_obj)
context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
blender_bones = {}
for bone_info in bones_data:
bl_bone = armature_data.edit_bones.new(name=bone_info['name'])
blender_bones[bone_info['name']] = bl_bone
for bone_info in bones_data:
bl_bone = blender_bones[bone_info['name']]
if bone_info['parent'] and bone_info['parent'] in blender_bones:
bl_bone.parent = blender_bones[bone_info['parent']]
sro_quat = Quaternion((bone_info['rot'][3], bone_info['rot'][0], bone_info['rot'][1], bone_info['rot'][2]))
sro_pos = Vector(bone_info['pos'])
bl_bone.matrix = Matrix.Translation(sro_pos) @ sro_quat.to_matrix().to_4x4()
bpy.ops.object.mode_set(mode='OBJECT')
print(f" [LOG] Armature létrehozva: {armature_obj.name}")
return armature_obj
# --- UI és Operátorok ---
class SROProperties(PropertyGroup):
def _autofind_worker(self, active_path):
if getattr(self, "is_autofinding", False) or not active_path or not os.path.exists(active_path): return
try:
setattr(self, "is_autofinding", True)
directory = os.path.dirname(active_path)
base_name = os.path.splitext(os.path.basename(active_path))[0]
bmt_path = os.path.join(directory, base_name + '.bmt')
if not self.import_bmt_filepath and os.path.exists(bmt_path): self.import_bmt_filepath = bmt_path
bsk_path = os.path.join(directory, base_name + '.bsk')
if not self.import_bsk_filepath and os.path.exists(bsk_path): self.import_bsk_filepath = bsk_path
bms_path = os.path.join(directory, base_name + '.bms')
if not self.import_bms_filepath and os.path.exists(bms_path): self.import_bms_filepath = bms_path
ddj_path = os.path.join(directory, base_name + '.ddj')
if not self.import_texture_filepath and os.path.exists(ddj_path): self.import_texture_filepath = ddj_path
finally:
setattr(self, "is_autofinding", False)
def bms_update(self, context): self._autofind_worker(self.import_bms_filepath)
def bmt_update(self, context): self._autofind_worker(self.import_bmt_filepath)
def bsk_update(self, context): self._autofind_worker(self.import_bsk_filepath)
def texture_update(self, context): self._autofind_worker(self.import_texture_filepath )
import_bms_filepath: StringProperty(name=".BMS File", subtype='FILE_PATH', description="Select a .bms model file", update=bms_update)
import_bmt_filepath: StringProperty(name=".BMT File", subtype='FILE_PATH', description="Select a .bmt material file", update=bmt_update)
import_bsk_filepath: StringProperty(name=".BSK File", subtype='FILE_PATH', description="Select a .bsk skeleton file", update=bsk_update)
import_texture_filepath: StringProperty(name="Texture File", subtype='FILE_PATH', description="Default texture if BMT is not used or texture is missing (.ddj only)", update=texture_update)
class SRO_OT_ImportUI(Operator):
bl_idname = "silkroad.import_bms"
bl_label = "Import Model"
def execute(self, context):
print(f"\n--- Új Importálási Folyamat Indul (v{bl_info['version'][0]}.{bl_info['version'][1]}.{bl_info['version'][2]}) ---")
if not PILLOW_OK: self.report({'ERROR'}, "Pillow library (PIL) is not installed. DDJ conversion will fail."); return {'CANCELLED'}
props = context.scene.sro_props
bms_path, bmt_path, bsk_path, texture_path_default = props.import_bms_filepath, props.import_bmt_filepath, props.import_bsk_filepath, props.import_texture_filepath
if not bms_path or not os.path.exists(bms_path):
print("[HIBA] Nincs .bms fájl kiválasztva, vagy a fájl nem létezik.")
self.report({'ERROR'}, "BMS file not selected or does not exist."); return {'CANCELLED'}
if not bms_path.lower().endswith('.bms'):
print(f"[HIBA] Érvénytelen BMS fájl: '{os.path.basename(bms_path)}'. Kérlek, .bms kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid file for BMS. Please select a .bms file."); return {'CANCELLED'}
if bmt_path and not bmt_path.lower().endswith('.bmt'):
print(f"[HIBA] Érvénytelen BMT fájl: '{os.path.basename(bmt_path)}'. Kérlek, .bmt kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid file for BMT. Please select a .bmt file."); return {'CANCELLED'}
if bsk_path and not bsk_path.lower().endswith('.bsk'):
print(f"[HIBA] Érvénytelen BSK fájl: '{os.path.basename(bsk_path)}'. Kérlek, .bsk kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid file for BSK. Please select a .bsk file."); return {'CANCELLED'}
if texture_path_default and not texture_path_default.lower().endswith('.ddj'):
print(f"[HIBA] Érvénytelen textúra fájl: '{os.path.basename(texture_path_default)}'. Kérlek, .ddj kiterjesztésű fájlt válassz.")
self.report({'ERROR'}, "Invalid default texture. Please select a .ddj file."); return {'CANCELLED'}
try:
bmt_data = read_bmt_file(bmt_path)
bones_data = read_bsk_file(bsk_path)
verts, normals, uvs, faces, bones, weights = [], [], [], [], [], []
mesh_name_from_bms, mat_name_from_bms = "", ""
print(f"[LOG] BMS fájl megnyitása: {os.path.basename(bms_path)}")
with open(bms_path, 'rb') as f:
if f.read(7) != b"JMXVBMS": raise ValueError("Not a valid BMS file signature.")
f.read(5)
header_offsets = struct.unpack('<10I', f.read(40))
header = {"vertex_offset": header_offsets[0], "skin_offset": header_offsets[1], "face_offset": header_offsets[2]}
print(f" -> Header beolvasva. Vertex offset: {header['vertex_offset']}, Skin offset: {header['skin_offset']}, Face offset: {header['face_offset']}")
f.read(8); vertex_flag = read_int(f); f.read(4)
mesh_name_from_bms, mat_name_from_bms = read_str(f), read_str(f)
f.read(4)
f.seek(header["vertex_offset"]); vcount = read_int(f)
print(f" [LOG] {vcount} vertex beolvasása...")
for _ in range(vcount):
verts.append(struct.unpack('<3f', f.read(12)))
normals.append(struct.unpack('<3f', f.read(12)))
uvs.append(struct.unpack('<2f', f.read(8)))
if vertex_flag & 0x400: f.read(8)
if vertex_flag & 0x800: f.read(36)
f.read(12)
print(f" -> Vertex adatok beolvasva: {len(verts)} pozíció, {len(normals)} normál, {len(uvs)} UV.")
f.seek(header["face_offset"]); fcount = read_int(f)
print(f" [LOG] {fcount} lap (face) beolvasása...")
faces = [tuple(reversed(tuple(read_short(f) for _ in range(3)))) for _ in range(fcount)]
print(f" -> Lap adatok beolvasva: {len(faces)} lap.")
if header["skin_offset"] > 0:
f.seek(header["skin_offset"]); bcount = read_int(f)
if bcount > 0:
print(f" [LOG] {bcount} csont (bone) és súlyozás beolvasása...")
bones = [read_str(f) for _ in range(bcount)]
print(f" -> Mesh-hez tartozó csontok: {', '.join(bones)}")
for _ in range(vcount):
bi1, bw1, bi2, bw2 = read_byte(f), read_short(f), read_byte(f), read_short(f)
total = bw1 + bw2 if (bw1 + bw2) > 0 else 1.0
weights.append((bi1, bw1/total, bi2, bw2/total))
print(f" -> Súlyozási adatok beolvasva {len(weights)} vertexhez.")
base_name = mesh_name_from_bms or os.path.splitext(os.path.basename(bms_path))[0]
container_obj = bpy.data.objects.new(base_name, None)
context.collection.objects.link(container_obj)
print("[LOG] Blender objektum létrehozása...")
mesh = bpy.data.meshes.new(base_name + '_Mesh')
mesh_obj = bpy.data.objects.new(base_name + "_Mesh", mesh)
mesh_obj.parent = container_obj
context.collection.objects.link(mesh_obj)
mesh.from_pydata(verts, [], faces); mesh.update()
if normals:
mesh.normals_split_custom_set_from_vertices(normal s)
mesh.shade_smooth()
print(" [LOG] Custom normals sikeresen alkalmazva.")
bm = bmesh.new(); bm.from_mesh(mesh)
uv_layer = bm.loops.layers.uv.new("UVMap")
for face in bm.faces:
for loop in face.loops:
loop[uv_layer].uv = (uvs[loop.vert.index][0], 1.0 - uvs[loop.vert.index][1])
bm.to_mesh(mesh); bm.free()
print(" [LOG] UV map sikeresen létrehozva.")
armature_obj = None
if bones_data:
armature_obj = create_armature(base_name, bones_data, context)
armature_obj.parent = container_obj
# JAVÍTÁS: A mesh objektumot tesszük újra aktívvá az anyagbeállítás előtt
context.view_layer.objects.active = mesh_obj
if bones and weights:
print(f" [LOG] Vertex csoportok létrehozása és súlyozás...")
for b_name in bones: mesh_obj.vertex_groups.new(name=b_name)
for i, w in enumerate(weights):
bi1, bw1, bi2, bw2 = w
if bw1 > 0.001 and bi1 < len(bones): mesh_obj.vertex_groups[bones[bi1]].add([i], bw1, 'REPLACE')
if bw2 > 0.001 and bi2 < len(bones): mesh_obj.vertex_groups[bones[bi2]].add([i], bw2, 'ADD')
if armature_obj:
modifier = mesh_obj.modifiers.new(name='Armature', type='ARMATURE')
modifier.object = armature_obj
print(f" [LOG] Súlyozás sikeresen alkalmazva.")
print("[LOG] Anyag létrehozása...")
mat_props = bmt_data.get(mat_name_from_bms)
final_mat_name = (mat_props.get('name') if mat_props else mat_name_from_bms) or "Material"
mat = bpy.data.materials.new(name=final_mat_name)
mat.use_nodes = True
mesh_obj.data.materials.append(mat)
nodes, links = mat.node_tree.nodes, mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
if not bsdf:
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
bms_dir = os.path.dirname(bms_path)
bmt_dir = os.path.dirname(bmt_path) if bmt_path else ""
def find_texture(texture_name, default_path):
if not texture_name: return default_path
candidate = os.path.join(bmt_dir, texture_name)
if os.path.exists(candidate): return candidate
candidate_bms = os.path.join(bms_dir, texture_name)
if os.path.exists(candidate_bms): return candidate_bms
print(f" [FIGYELEM] A '{texture_name}' textúra nem található a BMT/BMS mappában.")
return default_path
def process_texture(texture_path, texture_type="Diffuse"):
if not texture_path or not os.path.exists(texture_path) or not texture_path.lower().endswith('.ddj'):
return None
print(f" [LOG] {texture_type} textúra feldolgozása: {os.path.basename(texture_path)}")
try:
png_path = convert_ddj_to_png(texture_path)
print(f" -> Sikeresen konvertálva ide: {png_path}")
return png_path
except Exception as e:
print(f" [HIBA] A(z) {texture_type} textúra konvertálása sikertelen: {e}")
return None
diffuse_tex_name = mat_props.get('texture') if mat_props else None
diffuse_path = find_texture(diffuse_tex_name, texture_path_default)
png_path_diffuse = process_texture(diffuse_path, "Diffuse")
if png_path_diffuse:
tex_node = nodes.new("ShaderNodeTexImage")
tex_node.image = bpy.data.images.load(png_path_diffuse)
print(f" -> Kép betöltve a Blenderbe: {os.path.basename(png_path_diffuse)}")
mat.node_tree.nodes.active = tex_node
links.new(bsdf.inputs['Base Color'], tex_node.outputs['Color'])
if mat_props and mat_props['flags'] & 0x200:
mat.blend_method = 'BLEND'
if hasattr(mat, "eevee"):
mat.eevee.shadow_method = 'HASHED'
print(f" -> Diffuse textúra sikeresen hozzárendelve a shaderhez.")
normal_tex_name = mat_props.get('normal_map') if mat_props else None
if normal_tex_name:
normal_path = find_texture(normal_tex_name, "")
png_path_normal = process_texture(normal_path, "Normal Map")
if png_path_normal:
norm_tex_node = nodes.new("ShaderNodeTexImage")
norm_tex_node.image = bpy.data.images.load(png_path_normal)
norm_tex_node.image.colorspace_settings.name = 'Non-Color'
norm_map_node = nodes.new("ShaderNodeNormalMap")
links.new(norm_tex_node.outputs['Color'], norm_map_node.inputs['Color'])
links.new(norm_map_node.outputs['Normal'], bsdf.inputs['Normal'])
print(f" -> Normal Map sikeresen hozzárendelve a shaderhez.")
if mat_props:
bsdf.inputs['Base Color'].default_value = mat_props['diffuse']
bsdf.inputs['Specular IOR Level'].default_value = mat_props['specular'][0]
bsdf.inputs['Roughness'].default_value = max(0.0, min(1.0, 1.0 - (mat_props['shininess'] / 128.0)))
print("[LOG] Importálás befejezése, objektum elforgatása...")
container_obj.rotation_euler[0] = math.radians(-90)
self.report({'INFO'}, f"Successfully imported: {container_obj.name}")
print("--- Importálási Folyamat Befejeződött ---")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Import failed: {e}")
import traceback; traceback.print_exc()
return {'CANCELLED'}
class VIEW3D_PT_sro_panel(Panel):
bl_label="Silkroad Tools"; bl_idname="VIEW3D_PT_silkroad_panel"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category='Silkroad'
def draw(self, context):
props=context.scene.sro_props; box=self.layout.box()
box.label(text="Model & Skeleton Import", icon='IMPORT')
box.prop(props, "import_bms_filepath")
box.prop(props, "import_bsk_filepath")
box.prop(props, "import_bmt_filepath")
box.prop(props, "import_texture_filepath", text=".DDJ File")
box.operator(SRO_OT_ImportUI.bl_idname)
# --- Regisztráció ---
classes = (SROProperties, SRO_OT_ImportUI, VIEW3D_PT_sro_panel)
def register():
if not PILLOW_OK: print("WARNING: 'Pillow' Python library not installed. DDJ textures cannot be loaded.")
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.sro_props = PointerProperty(type=SROProperties)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.sro_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Silkroad JMX Importer",
"author": "szabo176",
"version": (3, 2, 7), # FINAL VERSION
"blender": (4, 5, 0),
"location": "View3D Sidebar > Silkroad",
"description": "Imports BMS models with BMT/DDJ support, orientation and vertex groups.",
"category": "Import-Export"
}
import bpy
import os
import bmesh
import struct
import tempfile
import io
import math
try:
from PIL import Image
PILLOW_OK = True
except ImportError:
PILLOW_OK = False
from bpy.props import StringProperty, PointerProperty, BoolProperty
from bpy.types import Operator, Panel, PropertyGroup
# --- Segédfüggvények ---
def read_int(f): return int.from_bytes(f.read(4), 'little')
def read_short(f): return int.from_bytes(f.read(2), 'little')
def read_byte(f): return int.from_bytes(f.read(1), 'little')
def read_float(f): return struct.unpack('<f', f.read(4))[0]
def read_color4(f): return struct.unpack('<4f', f.read(16))
def read_str(f):
str_len = read_int(f);
if str_len <= 0: return ""
str_bytes = f.read(str_len)
try: return str_bytes.decode("cp949")
except UnicodeDecodeError: return str_bytes.decode("utf-8", errors='ignore')
def name_exists(name): return name in bpy.data.objects
# --- Képfeldolgozás ---
def convert_ddj_to_png(ddj_path):
if not PILLOW_OK: raise ImportError("A Pillow (PIL) könyvtár szükséges.")
try:
with open(ddj_path, 'rb') as f:
f.seek(20); dds_data = f.read()
image = Image.open(io.BytesIO(dds_data))
temp_png_path = os.path.join(tempfile.gettempdir(), os.path.basename(ddj_path) + ".png")
image.save(temp_png_path, 'PNG')
return temp_png_path
except Exception as e:
print(f"DDJ -> PNG konverziós hiba: {e}"); return None
# --- BMT Fájl Értelmező ---
def read_bmt_file(bmt_filepath):
if not bmt_filepath or not os.path.exists(bmt_filepath): return {}
materials = {}
try:
with open(bmt_filepath, 'rb') as f:
if b"JMXVBMT" not in f.read(12): return {}
count = read_int(f)
for _ in range(count):
mat_name = read_str(f)
props = {
'name': mat_name, 'diffuse': read_color4(f), 'ambient': read_color4(f),
'specular': read_color4(f), 'emissive': read_color4(f),
'shininess': read_float(f), 'flags': read_int(f), 'texture': None
}
if props['flags'] & 0x100:
props['texture'] = read_str(f)
f.seek(7, 1)
materials[mat_name] = props
except Exception as e:
print(f"Hiba a BMT fájl olvasása közben: {e}")
return materials
# --- UI és Operátorok ---
class SROProperties(PropertyGroup):
import_bms_filepath: StringProperty(name=".BMS File", subtype='FILE_PATH')
import_bmt_filepath: StringProperty(name=".BMT File", subtype='FILE_PATH')
import_ddj_filepath: StringProperty(name=".DDJ|.DDS. PNG File", subtype='FILE_PATH')
use_alpha_blend: BoolProperty(name="Enable Transparency", default=False)
class SRO_OT_ImportUI(Operator):
bl_idname = "silkroad.import_bms"
bl_label = "Modell Importálása"
def execute(self, context):
print(f"\n--- Új Importálási Folyamat Indul (v{bl_info['version'][0]}.{bl_info['version'][1]}.{bl_info['version'][2]}) ---")
if not PILLOW_OK: self.report({'ERROR'}, "Pillow könyvtár hiányzik!"); return {'CANCELLED'}
props = context.scene.sro_props
bms_path, bmt_path, ddj_path = props.import_bms_filepath, props.import_bmt_filepath, props.import_ddj_filepath
if not bms_path or not os.path.exists(bms_path):
self.report({'ERROR'}, "BMS fájl nincs kiválasztva vagy nem létezik."); return {'CANCELLED'}
try:
bmt_data = read_bmt_file(bmt_path)
verts, uvs, faces, bones, weights = [], [], [], [], []
mesh_name_from_bms, mat_name_from_bms = "", ""
print(f"[LOG] BMS fájl megnyitása: {os.path.basename(bms_path)}")
with open(bms_path, 'rb') as f:
if b"JMXVBMS" not in f.read(12): raise ValueError("Nem érvényes BMS fájl.")
p_verticies, p_bones, p_faces = read_int(f), read_int(f), read_int(f)
for _ in range(7): read_int(f)
for _ in range(5): read_int(f)
mesh_name_from_bms, mat_name_from_bms = read_str(f), read_str(f)
f.seek(p_verticies); vcount = read_int(f)
print(f" [LOG] {vcount} vertex beolvasása...")
for _ in range(vcount):
verts.append((read_float(f), -read_float(f), read_float(f)))
f.seek(12, 1); u, v = read_float(f), read_float(f); uvs.append((u, 1.0 - v)); f.seek(12, 1)
f.seek(p_faces); fcount = read_int(f)
print(f" [LOG] {fcount} lap (face) beolvasása...")
faces = [tuple(read_short(f) for _ in range(3)) for _ in range(fcount)]
if p_bones > 0:
f.seek(p_bones); bcount = read_int(f)
if bcount > 0:
print(f" [LOG] {bcount} csont (bone) és súlyozás beolvasása...")
bones = [read_str(f) for _ in range(bcount)]
weights = [(read_byte(f), read_short(f), read_byte(f), read_short(f)) for _ in range(vcount)]
print("[LOG] BMS adatok sikeresen a memóriába olvasva.")
print("[LOG] Blender objektum létrehozása...")
obj_name = mesh_name_from_bms or os.path.basename(bms_path).split('.')[0]
mesh = bpy.data.meshes.new(obj_name + '_Mesh')
obj = bpy.data.objects.new(obj_name, mesh)
context.collection.objects.link(obj); context.view_layer.objects.active = obj
mesh.from_pydata(verts, [], faces); mesh.update()
bm = bmesh.new(); bm.from_mesh(mesh)
uv_layer = bm.loops.layers.uv.new("UVMap")
for face in bm.faces:
for loop in face.loops: loop[uv_layer].uv = uvs[loop.vert.index]
bm.to_mesh(mesh); bm.free()
if bones:
print(f" [LOG] Vertex csoportok létrehozása: {bones}")
for b_name in bones: obj.vertex_groups.new(name=b_name)
for i, w in enumerate(weights):
bi1, bw1, bi2, bw2 = w
if bw1 > 0 and bi1 < len(bones): obj.vertex_groups[bones[bi1]].add([i], bw1 / 10000.0, 'REPLACE')
if bw2 > 0 and bi2 < len(bones): obj.vertex_groups[bones[bi2]].add([i], bw2 / 10000.0, 'ADD')
print("[LOG] Geometria és súlyozás kész.")
print("[LOG] Anyag létrehozása...")
mat_props = bmt_data.get(mat_name_from_bms)
final_mat_name = (mat_props.get('name') if mat_props else mat_name_from_bms) or "Material"
mat = bpy.data.materials.new(name=final_mat_name)
mat.use_nodes = True; obj.data.materials.append(mat)
# VÉGLEGES JAVÍTÁS: Külön sorokban definiáljuk a változókat.
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
final_ddj_path = ddj_path
if mat_props and mat_props.get('texture'):
bmt_dir = os.path.dirname(bmt_path) if bmt_path else ""
path_from_bmt = os.path.join(bmt_dir, mat_props['texture'])
if os.path.exists(path_from_bmt): final_ddj_path = path_from_bmt
if final_ddj_path and os.path.exists(final_ddj_path):
png_path = convert_ddj_to_png(final_ddj_path)
if png_path and os.path.exists(png_path):
print(f" [LOG] Textúra node-ok létrehozása...")
tex_node = nodes.new("ShaderNodeTexImage")
tex_node.image = bpy.data.images.load(png_path)
tex_node.interpolation = 'Closest'
uv_map_node = nodes.new(type='ShaderNodeUVMap'); uv_map_node.uv_map = "UVMap"
mapping_node = nodes.new(type='ShaderNodeMapping')
links.new(uv_map_node.outputs['UV'], mapping_node.inputs['Vector'])
links.new(mapping_node.outputs['Vector'], tex_node.inputs['Vector'])
links.new(bsdf.inputs['Base Color'], tex_node.outputs['Color'])
if props.use_alpha_blend:
mat.blend_method = 'BLEND'
links.new(bsdf.inputs['Alpha'], tex_node.outputs['Alpha'])
if mat_props:
print(f" [LOG] BMT adatok alkalmazása: Specular/Roughness...")
bsdf.inputs['Base Color'].default_value = mat_props['diffuse']
specular_value = mat_props['specular'][0]
roughness_value = max(0.0, min(1.0, 1.0 - mat_props['shininess']))
bsdf.inputs['Specular IOR Level'].default_value = specular_value
bsdf.inputs['Roughness'].default_value = roughness_value
print(f" [LOG] Beállított Specular: {specular_value:.4f}, Roughness: {roughness_value:.4f}")
else:
bsdf.inputs['Base Color'].default_value = (0.8, 0.8, 0.8, 1.0)
self.report({'INFO'}, f"Sikeres import: {obj.name}")
print("--- Importálási Folyamat Befejeződött ---")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Importálás sikertelen: {e}"); import traceback; traceback.print_exc()
return {'CANCELLED'}
class VIEW3D_PT_sro_panel(Panel):
bl_label="Silkroad Eszközök"; bl_idname="VIEW3D_PT_silkroad_panel"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category='Silkroad'
def draw(self, context):
props=context.scene.sro_props; box=self.layout.box()
box.label(text="Modell Importálása", icon='IMPORT')
box.prop(props, "import_bms_filepath"); box.prop(props, "import_bmt_filepath"); box.prop(props, "import_ddj_filepath")
box.prop(props, "use_alpha_blend")
box.operator(SRO_OT_ImportUI.bl_idname)
# --- Regisztráció ---
classes = (SROProperties, SRO_OT_ImportUI, VIEW3D_PT_sro_panel)
def register():
if not PILLOW_OK: print("FIGYELEM: A 'Pillow' Python könyvtár nincs telepítve.")
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.sro_props = PointerProperty(type=SROProperties)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.sro_props
if __name__ == "__main__":
register()
"name": "Silkroad JMX Importer",
"author": "szabo176",
"version": (3, 2, 7), # FINAL VERSION
"blender": (4, 5, 0),
"location": "View3D Sidebar > Silkroad",
"description": "Imports BMS models with BMT/DDJ support, orientation and vertex groups.",
"category": "Import-Export"
}
import bpy
import os
import bmesh
import struct
import tempfile
import io
import math
try:
from PIL import Image
PILLOW_OK = True
except ImportError:
PILLOW_OK = False
from bpy.props import StringProperty, PointerProperty, BoolProperty
from bpy.types import Operator, Panel, PropertyGroup
# --- Segédfüggvények ---
def read_int(f): return int.from_bytes(f.read(4), 'little')
def read_short(f): return int.from_bytes(f.read(2), 'little')
def read_byte(f): return int.from_bytes(f.read(1), 'little')
def read_float(f): return struct.unpack('<f', f.read(4))[0]
def read_color4(f): return struct.unpack('<4f', f.read(16))
def read_str(f):
str_len = read_int(f);
if str_len <= 0: return ""
str_bytes = f.read(str_len)
try: return str_bytes.decode("cp949")
except UnicodeDecodeError: return str_bytes.decode("utf-8", errors='ignore')
def name_exists(name): return name in bpy.data.objects
# --- Képfeldolgozás ---
def convert_ddj_to_png(ddj_path):
if not PILLOW_OK: raise ImportError("A Pillow (PIL) könyvtár szükséges.")
try:
with open(ddj_path, 'rb') as f:
f.seek(20); dds_data = f.read()
image = Image.open(io.BytesIO(dds_data))
temp_png_path = os.path.join(tempfile.gettempdir(), os.path.basename(ddj_path) + ".png")
image.save(temp_png_path, 'PNG')
return temp_png_path
except Exception as e:
print(f"DDJ -> PNG konverziós hiba: {e}"); return None
# --- BMT Fájl Értelmező ---
def read_bmt_file(bmt_filepath):
if not bmt_filepath or not os.path.exists(bmt_filepath): return {}
materials = {}
try:
with open(bmt_filepath, 'rb') as f:
if b"JMXVBMT" not in f.read(12): return {}
count = read_int(f)
for _ in range(count):
mat_name = read_str(f)
props = {
'name': mat_name, 'diffuse': read_color4(f), 'ambient': read_color4(f),
'specular': read_color4(f), 'emissive': read_color4(f),
'shininess': read_float(f), 'flags': read_int(f), 'texture': None
}
if props['flags'] & 0x100:
props['texture'] = read_str(f)
f.seek(7, 1)
materials[mat_name] = props
except Exception as e:
print(f"Hiba a BMT fájl olvasása közben: {e}")
return materials
# --- UI és Operátorok ---
class SROProperties(PropertyGroup):
import_bms_filepath: StringProperty(name=".BMS File", subtype='FILE_PATH')
import_bmt_filepath: StringProperty(name=".BMT File", subtype='FILE_PATH')
import_ddj_filepath: StringProperty(name=".DDJ|.DDS. PNG File", subtype='FILE_PATH')
use_alpha_blend: BoolProperty(name="Enable Transparency", default=False)
class SRO_OT_ImportUI(Operator):
bl_idname = "silkroad.import_bms"
bl_label = "Modell Importálása"
def execute(self, context):
print(f"\n--- Új Importálási Folyamat Indul (v{bl_info['version'][0]}.{bl_info['version'][1]}.{bl_info['version'][2]}) ---")
if not PILLOW_OK: self.report({'ERROR'}, "Pillow könyvtár hiányzik!"); return {'CANCELLED'}
props = context.scene.sro_props
bms_path, bmt_path, ddj_path = props.import_bms_filepath, props.import_bmt_filepath, props.import_ddj_filepath
if not bms_path or not os.path.exists(bms_path):
self.report({'ERROR'}, "BMS fájl nincs kiválasztva vagy nem létezik."); return {'CANCELLED'}
try:
bmt_data = read_bmt_file(bmt_path)
verts, uvs, faces, bones, weights = [], [], [], [], []
mesh_name_from_bms, mat_name_from_bms = "", ""
print(f"[LOG] BMS fájl megnyitása: {os.path.basename(bms_path)}")
with open(bms_path, 'rb') as f:
if b"JMXVBMS" not in f.read(12): raise ValueError("Nem érvényes BMS fájl.")
p_verticies, p_bones, p_faces = read_int(f), read_int(f), read_int(f)
for _ in range(7): read_int(f)
for _ in range(5): read_int(f)
mesh_name_from_bms, mat_name_from_bms = read_str(f), read_str(f)
f.seek(p_verticies); vcount = read_int(f)
print(f" [LOG] {vcount} vertex beolvasása...")
for _ in range(vcount):
verts.append((read_float(f), -read_float(f), read_float(f)))
f.seek(12, 1); u, v = read_float(f), read_float(f); uvs.append((u, 1.0 - v)); f.seek(12, 1)
f.seek(p_faces); fcount = read_int(f)
print(f" [LOG] {fcount} lap (face) beolvasása...")
faces = [tuple(read_short(f) for _ in range(3)) for _ in range(fcount)]
if p_bones > 0:
f.seek(p_bones); bcount = read_int(f)
if bcount > 0:
print(f" [LOG] {bcount} csont (bone) és súlyozás beolvasása...")
bones = [read_str(f) for _ in range(bcount)]
weights = [(read_byte(f), read_short(f), read_byte(f), read_short(f)) for _ in range(vcount)]
print("[LOG] BMS adatok sikeresen a memóriába olvasva.")
print("[LOG] Blender objektum létrehozása...")
obj_name = mesh_name_from_bms or os.path.basename(bms_path).split('.')[0]
mesh = bpy.data.meshes.new(obj_name + '_Mesh')
obj = bpy.data.objects.new(obj_name, mesh)
context.collection.objects.link(obj); context.view_layer.objects.active = obj
mesh.from_pydata(verts, [], faces); mesh.update()
bm = bmesh.new(); bm.from_mesh(mesh)
uv_layer = bm.loops.layers.uv.new("UVMap")
for face in bm.faces:
for loop in face.loops: loop[uv_layer].uv = uvs[loop.vert.index]
bm.to_mesh(mesh); bm.free()
if bones:
print(f" [LOG] Vertex csoportok létrehozása: {bones}")
for b_name in bones: obj.vertex_groups.new(name=b_name)
for i, w in enumerate(weights):
bi1, bw1, bi2, bw2 = w
if bw1 > 0 and bi1 < len(bones): obj.vertex_groups[bones[bi1]].add([i], bw1 / 10000.0, 'REPLACE')
if bw2 > 0 and bi2 < len(bones): obj.vertex_groups[bones[bi2]].add([i], bw2 / 10000.0, 'ADD')
print("[LOG] Geometria és súlyozás kész.")
print("[LOG] Anyag létrehozása...")
mat_props = bmt_data.get(mat_name_from_bms)
final_mat_name = (mat_props.get('name') if mat_props else mat_name_from_bms) or "Material"
mat = bpy.data.materials.new(name=final_mat_name)
mat.use_nodes = True; obj.data.materials.append(mat)
# VÉGLEGES JAVÍTÁS: Külön sorokban definiáljuk a változókat.
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
final_ddj_path = ddj_path
if mat_props and mat_props.get('texture'):
bmt_dir = os.path.dirname(bmt_path) if bmt_path else ""
path_from_bmt = os.path.join(bmt_dir, mat_props['texture'])
if os.path.exists(path_from_bmt): final_ddj_path = path_from_bmt
if final_ddj_path and os.path.exists(final_ddj_path):
png_path = convert_ddj_to_png(final_ddj_path)
if png_path and os.path.exists(png_path):
print(f" [LOG] Textúra node-ok létrehozása...")
tex_node = nodes.new("ShaderNodeTexImage")
tex_node.image = bpy.data.images.load(png_path)
tex_node.interpolation = 'Closest'
uv_map_node = nodes.new(type='ShaderNodeUVMap'); uv_map_node.uv_map = "UVMap"
mapping_node = nodes.new(type='ShaderNodeMapping')
links.new(uv_map_node.outputs['UV'], mapping_node.inputs['Vector'])
links.new(mapping_node.outputs['Vector'], tex_node.inputs['Vector'])
links.new(bsdf.inputs['Base Color'], tex_node.outputs['Color'])
if props.use_alpha_blend:
mat.blend_method = 'BLEND'
links.new(bsdf.inputs['Alpha'], tex_node.outputs['Alpha'])
if mat_props:
print(f" [LOG] BMT adatok alkalmazása: Specular/Roughness...")
bsdf.inputs['Base Color'].default_value = mat_props['diffuse']
specular_value = mat_props['specular'][0]
roughness_value = max(0.0, min(1.0, 1.0 - mat_props['shininess']))
bsdf.inputs['Specular IOR Level'].default_value = specular_value
bsdf.inputs['Roughness'].default_value = roughness_value
print(f" [LOG] Beállított Specular: {specular_value:.4f}, Roughness: {roughness_value:.4f}")
else:
bsdf.inputs['Base Color'].default_value = (0.8, 0.8, 0.8, 1.0)
self.report({'INFO'}, f"Sikeres import: {obj.name}")
print("--- Importálási Folyamat Befejeződött ---")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Importálás sikertelen: {e}"); import traceback; traceback.print_exc()
return {'CANCELLED'}
class VIEW3D_PT_sro_panel(Panel):
bl_label="Silkroad Eszközök"; bl_idname="VIEW3D_PT_silkroad_panel"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category='Silkroad'
def draw(self, context):
props=context.scene.sro_props; box=self.layout.box()
box.label(text="Modell Importálása", icon='IMPORT')
box.prop(props, "import_bms_filepath"); box.prop(props, "import_bmt_filepath"); box.prop(props, "import_ddj_filepath")
box.prop(props, "use_alpha_blend")
box.operator(SRO_OT_ImportUI.bl_idname)
# --- Regisztráció ---
classes = (SROProperties, SRO_OT_ImportUI, VIEW3D_PT_sro_panel)
def register():
if not PILLOW_OK: print("FIGYELEM: A 'Pillow' Python könyvtár nincs telepítve.")
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.sro_props = PointerProperty(type=SROProperties)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.sro_props
if __name__ == "__main__":
register()







