**Hello everyone, I'm back again with an **UPDATE!
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:
3. Press **Enter**. You will see the system download and install the necessary packages. Wait for the process to finish.
4. To complete the installation, **restart Blender!** 🔄
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.
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)`.
• 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.
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.
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']]
# --- 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
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)
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'}
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'}
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)
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.")
# 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
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.")
# --- 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()
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)
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.")
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'])
# --- 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()
didn't read your code yet, but
why do you need Pillow?
blender loads .dds just fine
if your not going to directly convert ddj to dds to you local storage you can use tempfile
but often you will need to extract the dds from the dds buffer to use in photoshop and such, and with little python code you can extract them all in 2 minutes
easier, just saying
I know i was silly that time. But soon im going to release the full map importer plugin which doesnt use pillow anymore. You will need just a regular installed blender and my upcoming plugin, nothing else anymore.
I know i was silly that time. But soon im going to release the full map importer plugin which doesnt use pillow anymore. You will need just a regular installed blender and my upcoming plugin, nothing else anymore.
That's pretty cool.
How far will you go with this release? Any plans to actually make map creation and nvm support a thing?
[Re-Release] Sro DDJ Viewer , read and convert [ DAT , DSS , DDJ ] 02/02/2026 - SRO Coding Corner - 25 Replies I Think this wonderful tool is unknown for a lot of guys here ..
so i decided to share it ..
original Thread from : Here
*original Thread Copy Start*
Current Version : 0.8
.Net Framework 2.0 or higher required!
using DevIL image library.
[WIP] Silkroad File Formats (.bsr .bms .bmt .bsk .ban ) 03/29/2025 - SRO Coding Corner - 24 Replies I'm currently trying to work out some of the different structures of the files within the pk2's
http://i50.tinypic.com/xfybk9.jpg
.bsr Resource file. References meshes, materials, animations, skeletons etc.
.bms Meshes
.bmt Material file that references different .ddj textures
.ban Animation
.bsk Skeleton
Blender DDJ - BMS 09/11/2014 - SRO Private Server - 0 Replies hello guys i need a little help. After i edit my wepon model in bleder, i export the file and try to reconvert it back to a BMS file extension, but i always get error Index was outside bounds of the array. Can anyone guide me on what to do, would be greatly appropriated.
Request to closed thread, Probelm solved