Jump to content

decals.dat from 'The Swarm'.


Go to solution Solved by Serge25,

Recommended Posts

Posted

I have been struggling with the 'decals.dat' decals from the game 'The Swarm' (MorphX) for a long time.
I wrote a parser template for '010 Editor', it gives the values, but the decals are rotated
incorrectly (either the 3x2 matrix is incomplete in the data
or something else...). Maybe someone knows the solution?

decals.zip Swarm-Decals (BT).zip

  • Engineers
Posted
1 hour ago, Serge25 said:

Maybe someone knows the solution?

 

Hard to tell from what you provide, I see points only:

blender_y8RakkpCVW.thumb.png.129678aba12a9bf4bb40597577bf817b.png

Maybe focus on the akm_concrete_ objects (16, afaics), visualize them (including the rotations you've found) and compare the angles to the ones to be expected (maybe the concrete blocks build a wall in game?).

[63] 26 12.5 -13.6
[68] 26 13 -14.1
[69] 26 12.6 -14.2

...

Posted

Yes, these points are the positions for the projector (instead of it, I temporarily use a regular cube).
Decal meshes are created later, but that's not the point. The positions of the projector cubes
(they are also not all there for some reason:() and the scale is correct, but the rotation is wrong...
I do this 'project' in Unity (it's more convenient).

Posted

Here in this example you can see (here are two identical fragments of level one with
correct decals and it was captured through 'Ninja Ripper'), the other I recreate from the level data and decals.

Sample01.jpg

  • Engineers
Posted (edited)
1 hour ago, Serge25 said:

(here are two identical fragments of level

Thanks, but I don't see the two fragments in that picture. (Did you miss to upload the NJ screenshot?)

Edited by shak-otay
Posted

They are superimposed on each other - the screenshot shows their overlay. White is a piece of the level with NinjaRipper
(there are baked decals in it, one of which I highlighted in orange in the screenshot),
textured in a checkerboard texture, this is obtained in Unity from the chunk-mdl level of the game itself.
In this old game, I want to get the interesting ambient level.

  • Engineers
Posted

When you check

            Decals[57] = struct {
                    Name       = Char[32] ('akm_concrete_02\x00\x00\x00\x00\x00')
                    Position   = struct {
                        x          = Float(26.0)
                        y          = Float(13.125176429748535)
                        z          = Float(-11.330219268798828)
                    }
                    Rot        = struct {
                        m1          = Float(1.0)
                        m2         = Float(0.0)
                        m3          = Float(0.0)
                        m4          = Float(0.0)
                        m5          = Float(1.0)
                        m6          = Float(0.0)
                    }

same rot for Decals[63], [68], [69]..., [75],[79],[80]-[84]

Do they really all have the same rotations in game?

  • Solution
Posted

The issue is closed. The solution is to restore the first three rotation data (the first position in the 3x3 matrix). Here's the script for Blender v3.3:

#Script for Blender v3.3

import bpy
import os
import mathutils
import math
from mathutils import Matrix, Vector, Euler
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty

class TargemDecal:
    def __init__(self):
        self.name = ""
        self.position = Vector((0, 0, 0))
        self.rotation_matrix = [0.0] * 6
        self.scale = Vector((1, 1, 1))
        self.flags = 0

def read_null_terminated_string(data, offset, max_length=32):
    chars = []
    for i in range(max_length):
        char = data[offset + i]
        if char == 0:
            break
        chars.append(chr(char))
    return ''.join(chars), offset + max_length

def read_float(data, offset):
    import struct
    value = struct.unpack('<f', data[offset:offset+4])[0]
    return value, offset + 4

def read_uint(data, offset):
    import struct
    value = struct.unpack('<I', data[offset:offset+4])[0]
    return value, offset + 4

def parse_dat_file(filepath):
    decals = []
    
    try:
        with open(filepath, 'rb') as file:
            data = file.read()
        
        offset = 0
        
        magic_number, offset = read_uint(data, offset)
        decals_count, offset = read_uint(data, offset)
        
        print(f"Magic number: {magic_number}")
        print(f"Number of decals: {decals_count}")
        
        for i in range(decals_count):
            decal = TargemDecal()
            
            decal.name, offset = read_null_terminated_string(data, offset, 32)
            
            x, offset = read_float(data, offset)
            y, offset = read_float(data, offset)
            z, offset = read_float(data, offset)
            decal.position = Vector((x, y, z))
            
            for j in range(6):
                decal.rotation_matrix[j], offset = read_float(data, offset)
            
            scale_x, offset = read_float(data, offset)
            scale_y, offset = read_float(data, offset)
            scale_z, offset = read_float(data, offset)
            decal.scale = Vector((scale_x, scale_y, scale_z))
            
            decal.flags, offset = read_uint(data, offset)
            
            decals.append(decal)
            print(f"Decal {i}: {decal.name}, Pos: {decal.position}, Scale: {decal.scale}")
            
    except Exception as e:
        print(f"Error parsing file: {e}")
        return []
    
    return decals

def rotation_matrix_to_euler(rotation_matrix):
    try:
        v1 = Vector((rotation_matrix[0], rotation_matrix[1], rotation_matrix[2]))
        v2 = Vector((rotation_matrix[3], rotation_matrix[4], rotation_matrix[5]))
        
        v0 = v1.cross(v2)
        
        if v0.length > 0: v0.normalize()
        if v1.length > 0: v1.normalize()
        if v2.length > 0: v2.normalize()
        
        rotation_matrix_3x3 = Matrix((
            (v0.x, v1.x, v2.x),
            (v0.y, v1.y, v2.y),
            (v0.z, v1.z, v2.z)
        ))
        
        euler_rotation = rotation_matrix_3x3.to_euler('XYZ')
        
        return euler_rotation
        
    except Exception as e:
        print(f"Error converting rotation matrix: {e}")
        return Euler((0, 0, 0), 'XYZ')

def convert_to_blender_coordinates(position, rotation_matrix, scale):
    conversion_matrix = Matrix((
        (1, 0, 0),
        (0, 0, 1),
        (0, 1, 0)
    ))
    
    pos_vec = Vector((position.x, position.y, position.z))
    blender_position = conversion_matrix @ pos_vec
    
    scale_vec = Vector((scale.x, scale.y, scale.z))
    blender_scale = conversion_matrix @ scale_vec
    
    euler_rotation = rotation_matrix_to_euler(rotation_matrix)
    
    rotation_mat = euler_rotation.to_matrix()
    blender_rotation_mat = conversion_matrix @ rotation_mat @ conversion_matrix.transposed()
    blender_rotation = blender_rotation_mat.to_euler('XYZ')
    
    return blender_position, blender_rotation, blender_scale

def create_green_material():
    material_name = "GreenDecalMaterial"
    
    if material_name in bpy.data.materials:
        mat = bpy.data.materials[material_name]
    else:
        mat = bpy.data.materials.new(name=material_name)
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
        
        for node in nodes:
            nodes.remove(node)
        
        output_node = nodes.new(type='ShaderNodeOutputMaterial')
        principled_node = nodes.new(type='ShaderNodeBsdfPrincipled')
        
        principled_node.inputs[0].default_value = (0.0, 0.6, 0.0, 1.0)  # Base Color
        
        principled_node.inputs[9].default_value = 0.8  # Roughness
        
        mat.node_tree.links.new(principled_node.outputs['BSDF'], output_node.inputs['Surface'])
    
    mat.shadow_method = 'NONE'
    
    return mat

def create_decals_from_file(filepath):
    decals = parse_dat_file(filepath)
    
    if not decals:
        print("No decals found or error parsing file")
        return
    
    green_material = create_green_material()
    
    collection_name = os.path.basename(filepath)
    if collection_name in bpy.data.collections:
        decal_collection = bpy.data.collections[collection_name]
    else:
        decal_collection = bpy.data.collections.new(collection_name)
        bpy.context.scene.collection.children.link(decal_collection)
    
    for i, decal in enumerate(decals):
        blender_position, blender_rotation, blender_scale = convert_to_blender_coordinates(
            decal.position, decal.rotation_matrix, decal.scale
        )

        bpy.ops.mesh.primitive_cube_add(size=1.0, location=blender_position)
        cube = bpy.context.active_object
        
        cube.name = f"Decal_{i}_{decal.name}"
        
        cube.rotation_euler = blender_rotation
        cube.scale = blender_scale
        
        if cube.data.materials:
            cube.data.materials[0] = green_material
        else:
            cube.data.materials.append(green_material)
        
        if cube.name in bpy.context.scene.collection.objects:
            bpy.context.scene.collection.objects.unlink(cube)
        decal_collection.objects.link(cube)
        
        print(f"Created cube for decal {i}: {cube.name}")
    
    print(f"Successfully created {len(decals)} decal cubes")

class ImportTargemDecals(bpy.types.Operator, ImportHelper):
    bl_idname = "import_scene.targem_decals"
    bl_label = "Import Targem Decals"
    bl_options = {'PRESET', 'UNDO'}
    
    filename_ext = ".dat"
    
    filter_glob: StringProperty(
        default="*.dat",
        options={'HIDDEN'},
        maxlen=255,
    )
    
    def execute(self, context):
        filepath = self.filepath
        
        if not os.path.exists(filepath):
            self.report({'ERROR'}, f"File not found: {filepath}")
            return {'CANCELLED'}
        
        try:
            create_decals_from_file(filepath)
            self.report({'INFO'}, f"Successfully imported decals from {filepath}")
        except Exception as e:
            self.report({'ERROR'}, f"Error importing decals: {e}")
            return {'CANCELLED'}
        
        return {'FINISHED'}

def register():
    bpy.utils.register_class(ImportTargemDecals)

def unregister():
    bpy.utils.unregister_class(ImportTargemDecals)

if __name__ == "__main__":
    register()
    
    bpy.ops.import_scene.targem_decals('INVOKE_DEFAULT')

DecalsOK.jpg

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...