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.

[XBOX] Tenchu: Return from Darkness - .s4 3D Format

Featured Replies

  • Localization

I wanted to be able to modify the model, or convert it. I tried to get Noesis to read it, or Blender, but it doesn't work. It doesn't looks complex. If anyone can help, I would be grateful.

PL01.zip

Solved by h3x3r

  • Localization

The structure of the first 3/4 or so of the file is something like this (in ImHex Pattern Language). More sample files would make analyzing the rest of it easier.

struct Vertex {
    float position[3];
    float normal[3];
    float uv[2];
    float bone_weights[3];
    s16 bone_indices[4];
} [[single_color]];

struct Bone {
    u8 bytes[76];
} [[single_color]];

struct Mesh {
    u32 vertex_count;
    Vertex vertices[vertex_count];
    u32 tri_idx_count;
    u16 tri_strip_indices[tri_idx_count];
    u32 bone_count;
    Bone bones[bone_count];
};

struct File {
    u32 mesh_count;
    Mesh meshes[mesh_count];
};

File file_at_0x00 @ 0x00;

 

Edited by jmancoder

  • Author
  • Localization
1 hour ago, shak-otay said:

Did a quick check:

blender_PL01.thumb.png.850b745a3ccaa8117fec72b2228ad74f.png

Here's two more samples to help. Also, the textures from the models are inside them, in DDS format

Samples2.zip

  • Supporter
//------------------------------------------------
//--- 010 Editor v14.0 Binary Template
//
//      File: Tenchu: Return from Darkness
//   Authors: 
//   Version: 
//   Purpose: 
//  Category: 
// File Type: *.s4
//  ID Bytes: 
//   History: 
//------------------------------------------------
LittleEndian();OutputPaneClear();

local uint32 i,j,k,l,ElementBaseOffset,IndexBaseOffset;

uint32 MeshCount;

struct
{
    uint32 TotalElementCount;
    ElementBaseOffset=FTell();
    byte Elements[TotalElementCount * 52];
    
    uint32 TotalIndexCount;
    IndexBaseOffset=FTell();
    ushort Indices[TotalIndexCount];
    
    uint32 ShapeCount;
    
    struct
    {
        uint16 IndexOffset,IndexCount;
        uint32 ElementOffset,ElementCount;
        ubyte Unknown_0,Unknown_1,Unknown_2,Unknown_3;
        ubyte Unknown_4,Unknown_5,Unknown_6,Unknown_7;
        uint16 Unknown_8;
        ubyte Unknown_9;
        string ShapeName;
        FSeek(startof(ShapeName));
        FSkip(16);
        string TextureName;
        FSeek(startof(ShapeName));
        FSkip(53);
    }Shape[ShapeCount]<optimize=false>;
}Mesh[MeshCount]<optimize=false>;


struct
{
    uint32 BoneIndex;
    
    struct
    {
        string Name0;
        FSeek(startof(Name0));
        FSkip(32);
        string Name1;
        FSeek(startof(Name1));
        FSkip(32);
        ubyte BoneData[208];
    }Bone[BoneIndex]<optimize=false>;
}Skeleton;

struct
{
    uint32 Null;
    uint32 TextureDataSize;
    uint32 TextureIndex;
    
    struct
    {
        uint32 TextureSize;
        string TextureName;
        FSeek(startof(TextureName));
        FSkip(32);
    }TextureInfo[TextureIndex]<optimize=false>;
    struct
    {
        for (i=0; i < TextureIndex; i++)
        struct
        {
            byte TextureData[TextureInfo[i].TextureSize];
        }Texture;
    }TextureData;
}Textures;

I didn't bother with skeleton, but should be easy.

image.thumb.png.81245d9de4ebe24f86c4de4bd0b4e6aa.png

Well first part done. Now assign textures and skeleton

UV's are fine.

image.thumb.png.24505243553a6348aab9e8b92a8d9e6c.png

image.thumb.png.4399cd1d630ac819aafbd76524aa2c05.png

I dissabled LODs and shadow. Now it loads textures from file instead of externaly.

Also i parsed skeleton but there are 2 more Transform 4x4 so not sure which one is right one.

Edited by h3x3r

  • Supporter
  • Solution

Here's Noesis script. But without skeleton. I have no idea how to assign.

from inc_noesis import *
import noesis
import rapi
import os

def registerNoesisTypes():
   handle = noesis.register("Tenchu: Return from Darkness - Mesh", ".s4")
   noesis.setHandlerTypeCheck(handle, noepyCheckType)
   noesis.setHandlerLoadModel(handle, noepyLoadModel)
   noesis.logPopup()
   return 1
        
def noepyCheckType(data):
   bs = NoeBitStream(data)
   if len(data) < 20:
      return 0
   return 1
        
def noepyLoadModel(data, mdlList):
	bs = NoeBitStream(data)
	baseName = rapi.getExtensionlessName(rapi.getLocalFileName(rapi.getInputName()))
	ctx = rapi.rpgCreateContext()
	Underline = "_"
	TextureNameList = []
	TextureList = []
	MaterialList = []
    
	MeshIndex = bs.readUInt()
	MeshBaseOffset = bs.tell()
    
	# Skiping buffers
	for i in range(0, MeshIndex):
	    TECount = bs.readUInt()
	    bs.read(TECount * 52)
	    TICount = bs.readUInt()
	    bs.read(TICount * 2)
	    SCount = bs.readUInt()
	    bs.read(SCount * 76)
    
	# Skeleton
	BoneIndex = bs.readUInt()
	#print(BoneIndex)
	for b in range(0, BoneIndex):
	    BNameStrPos = bs.tell()
	    BoneName = bs.readString()
	    bs.seek(BNameStrPos, NOESEEK_ABS)
	    bs.read(32)
	    PBNameStrPos = bs.tell()
	    ParentBoneName = bs.readString()
	    bs.seek(PBNameStrPos, NOESEEK_ABS)
	    bs.read(32)
	    Transform4x4_0 = bs.read(64)
	    Transform4x4_1 = bs.read(64)
	    Transform4x4_2 = bs.read(64)
	    BoneIndex = bs.readInt()
	    ParentIndex = bs.readInt()
	    Unknown_0 = bs.readUInt()
	    Unknown_1 = bs.readUInt()
        
	# Texture info
	bs.read(4)
	TextureDataSize = bs.readUInt()
	TextureIndex = bs.readUInt()
	for t in range(0, TextureIndex):
		TextureSize = bs.readUInt()
		TexNamePos = bs.tell()
		TextureNameList.append(bs.readString())
		bs.seek(TexNamePos, NOESEEK_ABS)
		bs.read(32)
        
    # Texture data
	for t in range(0, TextureIndex):
		TextureName = TextureNameList[t]
		bs.read(12)
		TextureWidth = bs.readUInt()
		TexstureHeight = bs.readUInt()
		TextureDataSize = bs.readUInt()
		bs.read(60)
		PixelFormat = bs.readUInt()
		bs.read(40)
		TextureBuffer = bs.read(TextureDataSize)
		if PixelFormat == 894720068:
		    Texture = NoeTexture(TextureName, TextureWidth, TexstureHeight, TextureBuffer, noesis.NOESISTEX_DXT5)
		elif PixelFormat == 827611204:
		    Texture = NoeTexture(TextureName, TextureWidth, TexstureHeight, TextureBuffer, noesis.NOESISTEX_DXT1)
            
		TextureList.append(Texture)
        
	bs.seek(MeshBaseOffset, NOESEEK_ABS)
	for m in range(0, MeshIndex):
	    TotalElementCount = bs.readUInt()
	    ElementBaseOffset = bs.tell();
	    ElementBuffer = bs.read(TotalElementCount * 52)
                
	    TotalIndexCount = bs.readUInt()
	    IndexBaseOffset = bs.tell();
	    bs.read(TotalIndexCount * 2)
        
	    rapi.rpgBindPositionBufferOfs(ElementBuffer, noesis.RPGEODATA_FLOAT, 52, 0)
	    rapi.rpgBindNormalBufferOfs(ElementBuffer, noesis.RPGEODATA_FLOAT, 52, 12)
	    rapi.rpgBindUV1BufferOfs(ElementBuffer, noesis.RPGEODATA_FLOAT, 52, 24)
	    rapi.rpgBindBoneWeightBufferOfs(ElementBuffer, noesis.RPGEODATA_FLOAT, 52, 32, 3)
	    rapi.rpgBindBoneIndexBufferOfs(ElementBuffer, noesis.RPGEODATA_SHORT, 52, 44, 4)
        
	    ShapeIndex = bs.readUInt()
        
	    for s in range(0, ShapeIndex):
	    	IndexOffset = bs.readUShort() * 2
	    	IndexCount = bs.readUShort() + 2
	    	ElementOffset = bs.readUInt()
	    	ElementCount = bs.readUInt()
	    	MaterialIndex = bs.readUByte()
	    	bs.read(2)
	    	HasNoUV = bs.readUByte()
	    	bs.read(4)
	    	IsLOD = bs.readUShort()
	    	bs.read(1)
	    	ShapeNameCurPos = bs.tell()
	    	ShapeName = bs.readString()
	    	bs.seek(ShapeNameCurPos, NOESEEK_ABS)
	    	bs.read(16)
	    	TextureNameCurPos = bs.tell()
	    	TextureName = bs.readString()
	    	bs.seek(TextureNameCurPos, NOESEEK_ABS)
	    	bs.read(37)
	    	cPos = bs.tell()
	    	ShapeId = "{:04d}".format(s)
            
	    	Material = NoeMaterial(TextureName, TextureName)
	    	MaterialList.append(Material)
	    	rapi.rpgSetMaterial(TextureName)
	    	rapi.rpgSetName(baseName + Underline + ShapeName + Underline + ShapeId)
	    	bs.seek(IndexBaseOffset + IndexOffset, NOESEEK_ABS)
	    	IndexBuffer = bs.read(IndexCount * 2)
	    	if IsLOD != 3 and HasNoUV != 1:
	    	    rapi.rpgCommitTriangles(IndexBuffer, noesis.RPGEODATA_USHORT, IndexCount, noesis.RPGEO_TRIANGLE_STRIP)
	    	bs.seek(cPos, NOESEEK_ABS)
	mdl = rapi.rpgConstructModel()
	mdl.setModelMaterials(NoeModelMaterials(TextureList, MaterialList))
	mdlList.append(mdl)
	return 1

 

Edited by h3x3r

  • Supporter

Well, the hierarchy (taken from the script, with some corrections) is too confusing to me.

-1 "root" 1
0 "EX-00 "2
1 "00-jnt" 3
2 "01-jnt" 4
3 "04-jnt" 5
4 "05-jnt" 6
5 "06-jnt" 7
6 "07-jnt" 8
7 "EX-03 "9
8 "EX-04 "-1
9 "08-jnt" 11
10 "09-jnt" 12
11 "10-jnt" 13
12 "11-jnt" 14
13 "EX-05 "15
14 "EX-06 "-1
15 "02-jnt" 17
16 "03-jnt" -1
17 "12-jnt" 19
18 "13-jnt" 20
19 "14-jnt" 21
20 "15-jnt" 22
21 "16-jnt" 23
22 "EX-01 "-1
23 "17-jnt" -1
24 "18-jnt" 26
25 "19-jnt" 27
26 "20-jnt" 28
27 "21-jnt" 29
28 "EX-02 "-1
29 "22-jnt" -1

Rearranging doesn't do better (where it's unclear whether to take the boneID from the script, first column or the joint number, 4 form 04-jnt, for example)

-1 "root" 1
1 "00-jnt" 3
2 "01-jnt" 4
15 "02-jnt" 17
16 "03-jnt" -1
3 "04-jnt" 5
4 "05-jnt" 6
5 "06-jnt" 7
6 "07-jnt" 8
9 "08-jnt" 11
10 "09-jnt" 12
11 "10-jnt" 13
12 "11-jnt" 14
17 "12-jnt" 19
18 "13-jnt" 20
19 "14-jnt" 21
20 "15-jnt" 22
21 "16-jnt" 23
23 "17-jnt" -1
24 "18-jnt" 26
25 "19-jnt" 27
26 "20-jnt" 28
27 "21-jnt" 29
29 "22-jnt" -1
0 "EX-00 "2
22 "EX-01 "-1
28 "EX-02 "-1
7 "EX-03 "9
8 "EX-04 "-1
13 "EX-05 "15
14 "EX-06 "-1

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.