Skip to content
View in the app

A better way to browse. Learn more.

ResHax

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
Help us keep the site running.

(PS3) Killzone 2 Models/Textures (Early Decima Engine) Help Wanted

Featured Replies

  • Localization

With the Killzone crossover with Helldivers 2 being brought back, it reminded me of how I wanted to get the Killzone 2 (and maybe 3) models, getting access into the files isn't hard, typical PS3 stuff.

One big PSARC file, and that's about it.

Extracting it leads to getting the .core files that seem to contain everything (to my knowledge at least the models, and likely the textures, but the file sizes are a bit small), which later versions of the engine (Horizon Zero Dawn, Death Stranding, Until Dawn) seem to still use, although they've been certainly upgraded since this game. 

id-daemon did a tool for the PS4 Killzone game, Shadow Fall, but unfortunately that tool, nor the one made for Until Dawn, work with this game. 
Biggest problem seems to be a lot of packages reference each other. 

I'm a huge fan of the Killzone designs, and it's pretty surprising to me that over all these years no one has really made a true extractor for the older games, I'd love to work with these models but all that's really out there right now is random posts online with unrigged models and no real sources of where they came from. Due to the fact its PS3, I can't use Ninja Ripper, at least not to my knowledge.


Hope I'm not annoying anyone with creating some topic every few weeks, but I figure if anywhere would be a good place to talk about this stuff, it'd be here. Archiving stuff is awesome, and being able to work with 3D assets is one of my favorite things, plus porting is my #1 hobby, I'm sure I'm not the only one out there who just enjoys exploring game assets.

(Sorry for the external link here, I was having upload issues, even when trying to upload in a few parts I had problems)
I'll provide a bunch of example files, of weapons, and characters, and the one and only reference I've found about this version for the model/texture formats from an oldish GitHub repo.
https://drive.google.com/file/d/1rbuBPevvYP-XwiSDZSuL17t2g70YX9K0/view?usp=sharing
https://github.com/headassbtw/Mantle

Edited by DreamingOxxy
Adjusted some wording

  • DreamingOxxy changed the title to (PS3) Killzone 2 Models/Textures (Early Decima Engine) Help Wanted
  • Supporter
14 hours ago, DreamingOxxy said:

it's pretty surprising to me that over all these years no one has really made a true extractor for the older games

Yeah if you are fan of defining almost 800 resource types then go for it. I have faith in you...

  • Localization

I think you can use Ninja Ripper.

 

  • Author
  • Localization

I tried it out recently with the newest updates, but it doesn't work well with the game from my testing. UVs do not export correctly at all and there's no local mesh space support (which I don't think there ever will be). Maybe if I change around some settings I could get UVs but without the rest pose the models are mostly useless to me.

  • Localization

My apologies. 

  • 1 month later...
  • Author
  • Localization

I am very interested in getting these models, so I tried to do some more research myself with the lmited knowledge I have.
Looking more into these files, it seems all the data within the file is listed at the top, along with a bit of information on what int it is, image.png.52a2823fbfcc73a917fd5086732c6076.png

Along with that, it seems all the sections are divided by PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPimage.png.0effe7f34f8ba5520ab41dbca889867f.png

Though I'm not sure what exactly these are, I've attempted to find vertices and such, but I'm uncertain what I'm looking for, other than it being Big Endian (I assume)
I know some of these also contain textures

The characters. files seem to point towards what files are used (particles, sounds, etc. so not too important), and some extra info that likely only means something to the engine
The asset_description. files are what likely house everything, and are labelled pretty well, this format doesn't seem that bad but I've not the experience to understand it.

If it's any use, here's a file from the game, and an extracted OBJ of the same file someone did years ago.
assets_description.characters.hgh_miner_hgh_miner_character.zip and assets_description.characters.hghminer_character.zip

  • Supporter

These 0x50 are related to textures only.

If you want get the model you need to find correct vertices, unit vectors and face indices. Not sure about normals.

Also model use scale factor for vertices which is described in another resource.

The strings you mentioned are resource types. It use numeric file names.

Edited by h3x3r

  • Author
  • Localization
1 hour ago, h3x3r said:

These 0x50 are related to textures only.

If you want get the model you need to find correct vertices, unit vectors and face indices. Not sure about normals.

Also model use scale factor for vertices which is described in another resource.

The strings you mentioned are resource types. It use numeric file names.

Ah, I see. I didn't realize those were textures, makes sense then.

A scaling factor of verts is something I've never heard of before, I'm assuming it's a number that multiplies the vert positions? 

1 hour ago, shak-otay said:

Assumed mesh can be found but some more fiddling required:

hgh_miner.png.44f9f424254c88ac32a6962af6096956.png

I've been trying my best to find the vertices like this, but I haven't been able to, there's too much in there that throws me off, I've no idea where the vertices start.

  • Supporter
On 1/20/2026 at 5:57 PM, DreamingOxxy said:

, I've no idea where the vertices start.

I've no time to check it exactly, but you could trick around with this for hgh_miner core:

hgh_miner1.png.e49d2f3657e65024a57b11dd5dcb4414.png

 

edit: for different sub mesh, count 202 (uv address 0xafcd4, size uvb: 8, half floats) not used here (255 means disabled, so pressing UVs' button creates bogus)!

I'd start with searching for 0800030200 after which the uvs start (half float), at 0xa488c for example. (Then try to find the suiting vertex blocks.)

uvs-a488c.png.831a4152d888d0c23eef7373c64600f6.png

Edited by shak-otay

  • Supporter

I think I solved this on former Xentax already, years ago, where scaling was required:

mesh1.png.583720c75cf252f6256a7450751325cf.png

So I stop now to search in the past...

edit: did not find my post, only user luxox18 mentioned use with hex2obj. (Maybe id-daemon made a tool, don't remember exactly.)

Edited by shak-otay

  • Author
  • Localization
34 minutes ago, shak-otay said:

So I stop now to search in the past...

Alright fair enough, thank you for any information you can get, I've never heard of the scaling thing before.

The values you gave are extremely helpful, screenshots especially, I'm much more visual in terms of learning and getting to see it helps a lot. I'm still learning what patterns to look for and this helps.

  • Supporter

Here's list of *.core files i don't have unpacked so far. KZ2 only.  There's some resource types i can't parse properly so it breaks unpack procedure.

suljeva_village
suljeva_village\section_shared\assets_ai_data
suljeva_village\section_tunnels_trains\assets_optimized_art_st
suljeva_village\section_village4\assets_optimized_tunnelstrainsgp_tunnelstrainshi

tharsis_refinery
tharsis_refinery\section_refinery\assets_optimized_playercartbottommover_playercarttopmover_refineryst
tharsis_refinery\section_station\assets_optimized_art_st

the_cruiser
the_cruiser\section_cruiser\assets_optimized_bridgegpst_shafthiupper
the_cruiser\section_mall\assets_optimized_mallgpst_mallst
the_cruiser\section_shared\assets_optimized_game_skybox

the_new_sun
the_new_sun\section_01\assets_optimized_art_reflection_room
the_new_sun\section_01\assets_optimized_art_walkway

 

  • Author
  • Localization

Unpacked? That's insane news I wasn't expecting that! But I'm not too surprised by a few failures, even the tools id-daemon did for other Decima games ran into a lot of unpack crashes, Until Dawn especially gave me issues when I tried those tools. I know KZSF also had some strange texture formats that were never supported, but I'm not sure what about those would cause a failure, I've been busy with paperwork most of today so I didn't see this sooner, but if I had to guess it'd be something like map data, reflections, cubemaps, lighting, something of the sorts. Either way that's massive, and I can't thank you enough, I'm very excited.

  • Author
  • Localization
20 hours ago, h3x3r said:

Here's list of *.core files i don't have unpacked so far. KZ2 only.  There's some resource types i can't parse properly so it breaks unpack procedure.

Just curious, what was actually unpacked? Did you extract textures, meshes, armatures, etc.? Or is it just splitting them from their main files, so now you have multiple files rather than one big .core?

  • Supporter

image.thumb.png.cfca8438bdf8085189479bf0e15020d0.png

For RE is better keep them as individual files, but for model conversion it's better as one file.

  • Author
  • Localization

Ah, okay, the way they're structured is really interesting to look at like that, it's like Source engine almost, having all the properties like that. At least it seems straightforward from there, I presume with everything extracted like that it's easier to examine? Like being able to see info and all that. This is great either way. 

  • Author
  • Localization

Well, if anyone ever ends up making a script/template/etc. for these meshes, I'll be happy to hear, but I've thoroughly given up on my end, I've spent countless hours these past few weeks trying to learn, and have come up with nothing. This whole thing just seems not for me, throughout my various attempts at models, I've never been able to succeed and it just makes me more frustrated and worsens my depression.

I am also trying to figure out how these .core files work now.  The thing is killzone 3 (and other following Guerilla Games's games) uses updated (split: .core+.corestream) archive structure which is documented well enough besides we have special programs to take resources out of them. But killzone liberation and killzone 2 are not part of this deal.

So after some research (I'm not that skilled enough to easily crack it). I have this:

the first thing is RTTIBin header. Killzone Liberation uses RTTIBin<1.11>, Killzone 2 on PS3 uses RTTIBin<1.58>, Killzone 3 on PS3 uses RTTIBin<1.73> and Killzone: Shadow Fall uses RTTIBin<2.12>.

As far as I can tell they are different enough due to the PSP/PS3/PS4 architecture differences, but at the same time they are similar. CORE format is not a simple archive, but a Serialized Memory Image utilizing RTTI (Run-Time Type Information).

As far as I can tell .core file is a direct dump of game objects ready to be streamed into the PS3's RAM. It does not use a file table like a ZIP or PAK. Instead, it uses an Object Graph. Resources are referenced by 128-bit GUIDs or Indices, not by file paths. The structure of the file changes based on its Root Class. A Level Core (archive) looks different from a Character Core (asset definition).

As for Killzone 2 specifically I figured out this: 

Every file begins with a signature that defines the version of the Type System used (RTTIBin<1.58>) followed by 00 bytes to align to a 16-byte boundary. The RTTI Dictionary (String Table) Immediately following the header is a list of Class Names used in this specific file. Format: Null-terminated or Length-Prefixed ASCII strings.

This acts as a palette. Later in the file, objects reference these strings by Type Index (e.g., "Object #5 is Type #12").

Structural Variants. I discovered this during debugging of my extraction script. Seems like the data following the String Table depends on the file's purpose.

Variant A: The Container (Levels/Streaming Chunks)
Used for: sections, streaming_chunks. Features Contains a Dependency Table (Imports) and an Export Table. Uses signature: a block of GUIDs (16 bytes) followed by metadata.

Layout: [GUID (16b)] [Flags (4b)] [Padding] [Offset/Index]

Acts as a library. The GUIDs map external requests to internal memory offsets.

Variant B: The Asset (Characters/Weapons)
Features: Contains internal component names instead of external GUIDs. Signature seems like a Pascal-style Strings (Length-Prefixed).

Layout: [Total Size (1b)] [Length (1b)] [String Data]

Defines internal nodes like Bone Names (lowerJaw, neckRing), AI Nodes (AIHTNPlanner), and State definitions.

Killzone 2 definitely uses highly optimized vertex buffers for the RSX GPU. Vertex Declaration Signature: 08 00 03 02 00. This hex sequence marks the start of a UV Buffer definition. UVs (Texture Coordinates). Format: Half-Float (float16). Size: 4 bytes per vertex (U + V) found immediately after the signature (or aligned shortly after). Vertices (Positions): Format: Int16 (Signed Short). Structure: X, Y, Z (6 bytes total). Sometimes padded to 8 bytes. Scaling: Raw values are integers (e.g., 32000). They require a Scale Factor (a float vector stored in the parent Resource) to be converted to meters so that the 3d models don't look deformed. Faces (Indices): Likely Triangle Strips (for performance) encoded as uint16.

The scale factor is tricky though. As it was previously mentioned here if we miss it, the model will look like a microscopic dot or an exploding star (because raw integers 32000 will be interpreted as meters instead of centimeters). In theory we can fix scale during extraction or in Blender later.

First I tried to create an archive parser/extractor (python script), but failed. So later I switched the idea to create just a mesh extractor (also pyhon script) that would use the signature 08 00 03 02 00 to find the UV Buffers. Once a UV buffer is found, script calculates the Vertex Count (Size of UV Block / Stride). It searches the rest of the file for a Position Buffer that matches that exact Vertex Count. (e.g., if we have 500 UVs, we look for a block of 500 Positions), then it decodes UVs as Half-Floats (float16) and Positions as int16 (common on PS3) or float32.

BUT IT WON'T WORK (or I am missing something crucial). It only exports few jibberish files. I spent like two weeks trying to figure out what is right and what is wrong with the Killzone 2's .core archive structure, but for now I can only extract music files (they are standart .mp3s).  LOL

So any help towards where I am wrong and what I am missing is welcome. It would be much better to have a complete extraction script (I used pyhon).

here's  script if someone is interested in helping out

Spoiler
import struct
import os
import glob
import sys

# --- CONFIGURATION ---
DEBUG_LOG = True
# Common PS3 Texture Resolutions for Size Matching
TEXTURE_RESOLUTIONS = [(2048,2048), (1024,1024), (512,512), (256,256), (128,128), (64,64)]

class KZ2Extractor:
    def __init__(self, filepath):
        self.filepath = filepath
        self.filename = os.path.basename(filepath)
        self.data = None
        self.file_size = 0
        self.strings = []
        self.offset_table = []
        self.base_dir = f"{self.filename}_extracted"
        self.log_file = None

    def log(self, msg):
        if DEBUG_LOG:
            print(msg)
            if self.log_file: self.log_file.write(msg + "\n")

    def float16_to_float32(self, val_bytes):
        return struct.unpack('>e', val_bytes)[0]

    def create_dds_header(self, width, height, format_fourcc):
        # Generates a valid DXT1/DXT5 header to make PS3 textures readable on PC
        header = b'DDS ' + struct.pack('<I', 124) + struct.pack('<I', 0x000A1007)
        header += struct.pack('<II', height, width) + struct.pack('<I', width * height)
        header += struct.pack('<III', 0, 1, 0) + b'\x00' * 44
        header += struct.pack('<I', 32) + struct.pack('<I', 0x00000004) + format_fourcc
        header += b'\x00' * 20 + struct.pack('<I', 0x00001000) + b'\x00' * 16
        return header

    def detect_and_save_texture(self, data_chunk, offset):
        size = len(data_chunk)
        for w, h in TEXTURE_RESOLUTIONS:
            dxt1_size = (w * h) // 2
            dxt5_size = (w * h)
            
            # Allow 128 byte alignment variance
            if abs(size - dxt5_size) < 128:
                header = self.create_dds_header(w, h, b'DXT5')
                path = os.path.join(self.base_dir, "Textures", f"Tex_{hex(offset)}_{w}x{h}_DXT5.dds")
                with open(path, "wb") as f:
                    f.write(header)
                    f.write(data_chunk)
                return "Texture_DXT5"
            elif abs(size - dxt1_size) < 128:
                header = self.create_dds_header(w, h, b'DXT1')
                path = os.path.join(self.base_dir, "Textures", f"Tex_{hex(offset)}_{w}x{h}_DXT1.dds")
                with open(path, "wb") as f:
                    f.write(header)
                    f.write(data_chunk)
                return "Texture_DXT1"
        return None

    def scan_mesh_in_chunk(self, data_chunk, offset):
        # Scans a binary chunk for the vertex buffer signature
        # 08 00 03 02 00 (UV Definition Start)
        sig = b'\x08\x00\x03\x02\x00'
        idx = data_chunk.find(sig)
        if idx != -1:
            # found a mesh definition inside this object
            # This is a dumper that saves the raw geometry data for Blender import
            path = os.path.join(self.base_dir, "Models", f"Mesh_{hex(offset)}.bin")
            with open(path, "wb") as f:
                f.write(data_chunk)
            return "Mesh_Raw"
        return None

    def read_strings(self):
        # Skips header and reads strings until entropy change
        # Returns offset where strings end
        cursor = 16 # Skip RTTI Header
        self.log(f"[*] Parsing String Table...")
        
        while cursor < self.file_size:
            # Heuristic: Read until we hit non-printable block
            s_len = 0
            temp = b""
            valid = True
            
            check_cursor = cursor
            while True:
                b = self.data[check_cursor:check_cursor+1]
                if b == b'\x00': break
                if not (b' ' <= b <= b'~'): 
                    valid = False
                    break
                temp += b
                check_cursor += 1
                s_len += 1
                if s_len > 128: # Strings too long? Probably garbage
                    valid = False
                    break
            
            if valid and s_len > 2:
                try:
                    s = temp.decode('utf-8')
                    self.strings.append(s)
                    cursor = check_cursor 
                    # Skip nulls/padding
                    while cursor < self.file_size and self.data[cursor] < 32:
                        cursor += 1
                except:
                    cursor += 1
            else:
                # If we hit a block of garbage, we might be done with strings
                if len(self.strings) > 5 and s_len == 0:
                    # Check if next bytes look like GUIDs or Integers
                    # Align to 4 bytes
                    while cursor % 4 != 0: cursor += 1
                    return cursor
                cursor += 1
        return cursor

    def extract_by_offsets(self, table_start, count):
        self.log(f"[*] Extracting {count} Objects based on Offset Table at {hex(table_start)}")
        
        offsets = []
        for i in range(count):
            off = struct.unpack('>I', self.data[table_start + i*4 : table_start + i*4 + 4])[0]
            offsets.append(off)
        
        # Sort offsets to process sequentially
        offsets_sorted = sorted(list(set(offsets)))
        
        saved_count = 0
        
        for i in range(len(offsets_sorted)):
            start = offsets_sorted[i]
            if start >= self.file_size: continue
            
            if i < len(offsets_sorted) - 1:
                end = offsets_sorted[i+1]
            else:
                end = self.file_size
                
            chunk_size = end - start
            if chunk_size <= 0: continue
            
            chunk = self.data[start:end]
            
            # === TYPE DETECTION & SAVING ===
            res_type = "Unknown"
            
            # 1. Texture Check
            tex = self.detect_and_save_texture(chunk, start)
            if tex: res_type = tex
            
            # 2. Mesh Check (if not texture)
            if res_type == "Unknown":
                mesh = self.scan_mesh_in_chunk(chunk, start)
                if mesh: res_type = mesh
            
            # 3. Audio Check
            if chunk[0:4] == b'XVAG':
                with open(os.path.join(self.base_dir, "Audio", f"Snd_{hex(start)}.xvag"), "wb") as f:
                    f.write(chunk)
                res_type = "Audio_XVAG"
            
            # 4. Fallback: Save as generic binary if large enough
            if res_type == "Unknown" and chunk_size > 64:
                with open(os.path.join(self.base_dir, "Generic", f"Obj_{hex(start)}.bin"), "wb") as f:
                    f.write(chunk)
            
            if res_type != "Unknown":
                self.log(f"    Saved {hex(start)} as {res_type} ({chunk_size} bytes)")
                saved_count += 1

        self.log(f"[+] Extraction Complete. Saved {saved_count} identified resources.")

    def run(self):
        print(f"\n=== Processing {self.filename} ===")
        try:
            os.makedirs(self.base_dir, exist_ok=True)
            for folder in ["Textures", "Models", "Audio", "Generic", "AI_Logic"]:
                os.makedirs(os.path.join(self.base_dir, folder), exist_ok=True)
            
            self.log_file = open(os.path.join(self.base_dir, "extraction.log"), "w")
            
            with open(self.filepath, "rb") as f:
                self.data = f.read()
                self.file_size = len(self.data)
                
            # 1. Parse Strings (Dictionary)
            table_candidate_start = self.read_strings()
            self.log(f"[-] Strings end approx at {hex(table_candidate_start)}")
            
            # 2. Smart Offset Table Scan
            # We look for [Count] followed by [Offset 1] [Offset 2]...
            # assume the table is aligned 16 bytes after strings
            start_scan = (table_candidate_start // 16) * 16
            limit = min(start_scan + 50000, self.file_size)
            
            table_found = False
            
            for ptr in range(start_scan, limit, 4):
                # Candidate for Object Count
                cnt = struct.unpack('>I', self.data[ptr:ptr+4])[0]
                
                # Heuristic: Valid count range
                if 10 < cnt < 10000:
                    # Check first few offsets
                    valid_offsets = True
                    prev_off = 0
                    for k in range(min(cnt, 5)):
                        off_ptr = ptr + 4 + (k*4)
                        if off_ptr + 4 > self.file_size: 
                            valid_offsets = False
                            break
                        off_val = struct.unpack('>I', self.data[off_ptr:off_ptr+4])[0]
                        
                        if off_val >= self.file_size or off_val < ptr or off_val <= prev_off:
                            valid_offsets = False
                            break
                        prev_off = off_val
                    
                    if valid_offsets:
                        self.log(f"[!] Offset Table found at {hex(ptr)}. Count: {cnt}")
                        self.extract_by_offsets(ptr + 4, cnt)
                        table_found = True
                        break
            
            if not table_found:
                self.log("[!] Standard Offset Table not found. Attempting Raw Slice...")
                # Fallback: Just slice file by zeros/signatures if structure is too weird
                pass 

        except Exception as e:
            self.log(f"[FATAL ERROR] {e}")
        finally:
            if self.log_file: self.log_file.close()

if __name__ == "__main__":
    files = glob.glob("*.core")
    if not files:
        print("No .core files found.")
    else:
        for f in files:
            extractor = KZ2Extractor(f)
            extractor.run()
    input("Press Enter to close...")

 

 

Edited by math-daemon
+ script code

  • Supporter

I think for Killzone 2 meshes it's just a matter of patience and time which I don't have:

(assets_description.characters.hgh_miner_hgh_miner_character.core)

KZ2-miner-lowerbody.png

Edited by shak-otay

  • Supporter

KZ3 use same file system as KZ2 just with one difference. It has *.corestream which include higher assets. Mainly textures.

  • 1 month later...
  • Localization

Here you go - my Blender plugin on GitHub
Animations are not supported since they are in Sony's proprietary EDGE format

Feel free to use it as a reference for further research

  • Author
  • Localization
4 hours ago, other1 said:

Here you go - my Blender plugin on GitHub
Animations are not supported since they are in Sony's proprietary EDGE format

Feel free to use it as a reference for further research

This is great, amazing even, it works, the only thing I'm noticing is meshes with multiple UVs don't seem to import the first UV correctly? For example, some of the character models (the hooded sniper was the one I noticed immediately) in assets_description.onlinecharacters_onlinecharacters_concrete.core have a completely blank first UV

edit: To further explain this being an issue, the second UV is just a model projection, the actual texture coordinates are missing

Edited by DreamingOxxy

  • Author
  • Localization

I found Radec and a pistol in levels.single_player.tharsis_refinery.section_interogation.assets_optimized_hghradec_hghsta18pistol.core
Found the Helghast Sniper, and several weapons in assets_description.onlinecharacters_onlinecharacters_concrete.core
UVs for these two characters do not import, same goes with the several weapon meshes and the pistol.

edit: Further investigation, it seems some models just outright render their UVs differently? Even using a 3D ripper, VulkanRipper and NinjaRipper, the Sniper Helghast has no UVs, but the rest of the characters around seem to rip with them.

Edited by DreamingOxxy
More info

  • Author
  • Localization

Although it has UV issues, the model importer works really well, and can read some of the older models (v1.56) from the beta versions of KZ2, showing Radec with a scarf-cape. image.thumb.png.f81edbc06995a99be1e8e505f6da503d.pngimage.thumb.png.8f4c07847390c07fbfd45a08a975379a.png

Create an account or sign in to comment

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.