Author Topic: [FF7, Field, PSX, Animation] BCX/BSX file formats?  (Read 16015 times)

Micky

  • *
  • Posts: 300
    • View Profile
I'm trying to get BCX/BSX files displayed in my own viewer. I found my original code to read the mesh, but it looks like I didn't get animations to work. Searching here I found several people's decoding attempts, but most of the links are dead now. Does anyone still have his notes, or can update the Wiki with the information?
By the way, where in q-gears is the code to extract the information from the original disks? The SVN version I have seems to load the extracted data directly.

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #1 on: 2009-09-15 18:21:40 »
I added converter to SVN. Check it out.

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #2 on: 2009-09-16 06:59:11 »
Great, thank you! I've got it now.

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #3 on: 2009-09-28 21:48:12 »
Just found another bit: the byte after the vertex count in each part seems to be the number of texcoords.

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #4 on: 2009-10-20 21:49:34 »
After a bit of more exploration it looks like the unused byte after each vertex colour could be the normal index. The index has similar values as the vertex index (i.e. if two vertices have the same indices, the corresponding unused values are the same as well), but the normal table doesn't seem to be in the model. Maybe it is shared across all models and stored in the executable?
Another bit: The index goes from 0 to 240 for most BCX. Only Red 13 has a few less.
It's of course possible as well that the value directly encodes the value, and is split into two angle values.
« Last Edit: 2009-10-21 20:49:44 by Micky »

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #5 on: 2009-10-22 16:03:45 »
After a bit of more exploration it looks like the unused byte after each vertex colour could be the normal index. The index has similar values as the vertex index (i.e. if two vertices have the same indices, the corresponding unused values are the same as well), but the normal table doesn't seem to be in the model. Maybe it is shared across all models and stored in the executable?
Another bit: The index goes from 0 to 240 for most BCX. Only Red 13 has a few less.
It's of course possible as well that the value directly encodes the value, and is split into two angle values.

I can't see if normals used at all. Data set into GPU packets directly. I continuing reversing data related to initialization of field models. Stay tuned and look into SVN =)

By the way did you found something about additional file in texture data in BSX?
I found it reading in asm, but cant find file where it used.

It used in in texture data if 5-7 bytes are not zeros (this is offset to something). 4th Byte are number of textures.
« Last Edit: 2009-10-22 17:54:54 by Akari »

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #6 on: 2009-10-22 18:49:53 »
I can't see if normals used at all. Data set into GPU packets directly. I continuing reversing data related to initialization of field models. Stay tuned and look into SVN =)
I think the main character is "full bright" most of the time, but I'm sure I've seen shading in some of the areas where you see the characters large. But it would explain why I can't find a normal table. I'll have to double-check tonight. I can't imagine why they would add normals and lighting information to the PC version when it didn't exist in the PSX version.
At the moment I regenerate the normals based on face normals and shared vertices.

I'll try to get my code more stable and release it. at the moment I seem to run into some special cases when decoding BSX files. (For example vertex count being 0.)

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #7 on: 2009-10-22 19:13:35 »
I can't see if normals used at all. Data set into GPU packets directly. I continuing reversing data related to initialization of field models. Stay tuned and look into SVN =)
I think the main character is "full bright" most of the time, but I'm sure I've seen shading in some of the areas where you see the characters large. But it would explain why I can't find a normal table. I'll have to double-check tonight. I can't imagine why they would add normals and lighting information to the PC version when it didn't exist in the PSX version.
At the moment I regenerate the normals based on face normals and shared vertices.

I'll try to get my code more stable and release it. at the moment I seem to run into some special cases when decoding BSX files. (For example vertex count being 0.)

Lighting information calculated during model initialization and stored into GPU draft packets. It can be changed via KAWAI opcode which is recalculate lighting for all vertexes and store it to drafts.

Additional part in texture can be used to change global eye texture (FIELD.TDB). You can change image, palette. There is option to store given texture to anywhere in VRAM directly. And there is one more option which I can't understand. mystery solved (mostly) =)

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #8 on: 2009-10-24 16:57:10 »
Okay, for your amusement, a BCX/BSX viewer in Python:
Code: [Select]
import sys, struct, array, math, os, zlib

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL.ARB.texture_rectangle import *
from OpenGL.GL.ARB.multitexture import *

import pygame
from pygame.locals import *

bcxMap = {}
normals = []

def vecDot(a,b):
    return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]

def vecNormal(a):
    lenSqr = vecDot(a,a)
    if lenSqr > 0.0:
ool = 1.0 / math.sqrt(lenSqr)
return (a[0] * ool, a[1] * ool, a[2] * ool)
    else:
return (0,0,0)

def loadLzs(name):
    # read compressed data
    f = open(name, "rb")
    buf = f.read()
    f.close()
    
    (size,) = struct.unpack_from("<I", buf, 0)
    if size + 4 != len(buf):
del buf
return None

    ibuf = array.array("B", buf)
    obuf = array.array("B")
    
    # decompress;
    iofs = 4
    cmd = 0
    bit = 0
    while iofs < len(ibuf):
if bit == 0:
   cmd = ibuf[iofs]
   bit = 8
   iofs += 1
if cmd & 1:
   obuf.append(ibuf[iofs])
   iofs += 1
else:
   a = ibuf[iofs]
   iofs += 1
   b = ibuf[iofs]
   iofs += 1
   o = a | ((b & 0xF0) << 4)
   l = (b & 0xF) + 3;

   rofs = len(obuf) - ((len(obuf) - 18 - o) & 0xFFF)
   for j in xrange(l):
if rofs < 0:
   obuf.append(0)
else:
   obuf.append(obuf[rofs])
rofs += 1
cmd >>= 1
bit -= 1

    return obuf.tostring()

def loadFieldBin(name):
    # read compressed data
    f = open(name, "rb")
    buf = f.read()
    f.close()
    
    (size,) = struct.unpack_from("<I", buf, 0)

    # decompress
    data = zlib.decompress(buf[8:], 16 | 15)
    if len(data) != size:
del buf
del data
return None
    return data
  
def readModel(data, num_bones, offset_skeleton, num_parts, offset_parts):
    model = {}
    
    skeleton = []
    for i in xrange(num_bones):
(length, parent, has_mesh) = struct.unpack_from("<hbB", data, offset_skeleton + i * 4)
skeleton.append((length, parent, has_mesh != 0))
    model["skeleton"] = skeleton
    
    parts = []
    for i in xrange(num_parts):
part = {}
(u1, bone_index, num_vertex, num_texcoord, num_quad_color_tex, num_tri_color_tex, num_quad_mono_tex, num_tri_mono_tex, num_tri_mono, num_quad_mono, num_tri_color, num_quad_color, u3, offset_poly, offset_texcoord, offset_flags, offset_control, u4, offset_vertex) = struct.unpack_from("<BBBBBBBBBBBBHHHHHHH", data, offset_parts + i * 0x20)
# print (u1, bone_index, num_vertex, num_texcoord, num_quad_color_tex, num_tri_color_tex, num_quad_mono_tex, num_tri_mono_tex, num_tri_mono, num_quad_mono, num_tri_color, num_quad_color, u3, offset_poly, offset_texcoord, offset_flags, offset_control, u4, offset_vertex)
#print "offset_vertex %x %x %x" % (offset_vertex, num_vertex, offset_vertex + 4 + num_vertex * 8)
#print "offset_texcoord %x %x %x" % (offset_vertex + offset_texcoord, num_texcoord, offset_vertex + offset_texcoord + num_texcoord * 2)
part["bone_index"] = bone_index
# print (u1, u3, u4)

vertices = []
for j in xrange(num_vertex):
   (x, y, z) = struct.unpack_from("<hhh", data, offset_vertex + 4 + j * 8)
   vertices.append((x,y,z))
part["vertices"] = vertices

texcoords = []
for j in xrange(num_texcoord):
   (u, v) = struct.unpack_from("<BB", data, offset_vertex + offset_texcoord + j * 2)
   texcoords.append((u,v))
part["texcoords"] = texcoords

cur_poly = offset_vertex + offset_poly
cur_flags = offset_vertex + offset_flags
cur_control = offset_vertex + offset_control

# print "u4 : %4.4x" % (offset_vertex + u4)
# print "cur_poly : %4.4x" % (cur_poly)
# print "cur_flags : %4.4x" % (cur_flags)
# print "cur_control : %4.4x" % (cur_control)

quad_color_tex = []
for j in xrange(num_quad_color_tex):
   control = struct.unpack_from("<B", data, cur_control)
   cur_control += 1

   (av, bv, cv, dv, ar, ag, ab, an, br, bg, bb, bn, cr, cg, cb, cn, dr, dg, db, dn, at, bt, ct, dt) = struct.unpack_from("<BBBBBBBBBBBBBBBBBBBBBBBB", data, cur_poly)
   poly = ((av, at, (ar, ag, ab), an), (bv, bt, (br, bg, bb), bn), (dv, dt, (dr, dg, db), dn), (cv, ct, (cr, cg, cb), cn))

   # print "quad_color_tex %i %i %i %i / %i %i %i %i" % (av, bv, cv, dv, an, bn, cn, dn)

   quad_color_tex.append(poly)
   cur_poly += 0x18
part["quad_color_tex"] = quad_color_tex

tri_color_tex = []
for j in xrange(num_tri_color_tex):
   control = struct.unpack_from("<B", data, cur_control)
   cur_control += 1

   (av, bv, cv, xv, ar, ag, ab, an, br, bg, bb, bn, cr, cg, cb, cn, at, bt, ct, xt) = struct.unpack_from("<BBBBBBBBBBBBBBBBBBBB", data, cur_poly)
   poly = ((av, at, (ar, ag, ab), an), (bv, bt, (br, bg, bb), bn), (cv, ct, (cr, cg, cb), cn), xv, xt)
   # print "tri_color_tex %i %i %i / %x %x %x" % (av, bv, cv, an, bn, cn)

   tri_color_tex.append(poly)
   cur_poly += 0x14
part["tri_color_tex"] = tri_color_tex

quad_mono_tex = []
for j in xrange(num_quad_mono_tex):
   control = struct.unpack_from("<B", data, cur_control)
   cur_control += 1

   (av, bv, cv, dv, r, g, b, n, at, bt, ct, dt) = struct.unpack_from("<BBBBBBBBBBBB", data, cur_poly)
   # print "quad_mono_tex", x
   poly = ((av, at), (bv, bt), (dv, dt), (cv, ct), (r, g, b), n)
   quad_mono_tex.append(poly)
   cur_poly += 0x0C
part["quad_mono_tex"] = quad_mono_tex

tri_mono_tex = []
for j in xrange(num_tri_mono_tex):
   control = struct.unpack_from("<B", data, cur_control)
   cur_control += 1

   (av, bv, cv, xv, r, g, b, n, at, bt, ct, xt) = struct.unpack_from("<BBBBBBBBBBBB", data, cur_poly)
   poly = ((av, at), (bv, bt), (cv, ct), (r, g, b), n, xv, xt)
   # print "tri_mono_tex", x, xv, xt
   tri_mono_tex.append(poly)
   cur_poly += 0x0C
part["tri_mono_tex"] = tri_mono_tex

tri_mono = []
for j in xrange(num_tri_mono):
   (av, bv, cv, xv, r, g, b, n) = struct.unpack_from("<BBBBBBBB", data, cur_poly)
   poly = ((av,), (bv,), (cv,), (r, g, b), n, xv)
   # print "tri_mono", x, xv
   tri_mono.append(poly)
   cur_poly += 8
part["tri_mono"] = tri_mono

quad_mono = []
for j in xrange(num_quad_mono):
   (av, bv, cv, dv, r, g, b, n) = struct.unpack_from("<BBBBBBBB", data, cur_poly)
   poly = ((av,), (bv,), (dv,), (cv,), (r, g, b), n)
   # print "quad_mono", x
   quad_mono.append(poly)
   cur_poly += 8
part["quad_mono"] = quad_mono

tri_color = []
for j in xrange(num_tri_color):
   (av, bv, cv, xv, ar, ag, ab, an, br, bg, bb, bn, cr, cg, cb, cn) = struct.unpack_from("<BBBBBBBBBBBBBBBB", data, cur_poly)
   poly = ((av,(ar, ag, ab), an), (bv, (br, bg, bb), bn), (cv, (cr, cg, cb), cn), xv)
   # print "tri_color %i %i %i / %i %i %i %i / %i" % (av, bv, cv, an, bn, cn, xv, len(vertices))

   tri_color.append(poly)
   cur_poly += 0x10
part["tri_color"] = tri_color

quad_color = []
for j in xrange(num_quad_color):
   (av, bv, cv, dv, ar, ag, ab, an, br, bg, bb, bn, cr, cg, cb, cn, dr, dg, db, dn) = struct.unpack_from("<BBBBBBBBBBBBBBBBBBBB", data, cur_poly)
   poly = ((av,(ar, ag, ab), an), (bv, (br, bg, bb), bn), (dv, (dr, dg, db), dn), (cv, (cr, cg, cb), cn))

   # print "quad_color %i %i %i %i / %x %x %x %x" % (av, bv, cv, dv, an, bn, cn, dn)

   quad_color.append(poly)
   cur_poly += 0x14
part["quad_color"] = quad_color

# print "end_poly : %4.4x" % (cur_poly)
# print "end_flags : %4.4x" % (cur_flags)
# print "end_control : %4.4x" % (cur_control)

parts.append(part)

    model["parts"] = parts
    
    return model
    
def readAnimations(data, num_animations, offset_animations):
    animations = []
    for i in xrange(num_animations):
(num_frames, num_channel, num_frames_translation, num_static_translation, num_frames_rotation, offset_frames_translation, offset_static_translation, offset_frames_rotation, offset_data) = struct.unpack_from("<HBBBBHHHI", data, offset_animations + i * 0x10)
offset_data &= 0x7FFFFFFF
# print "animation:", (offset_animations + i * 0x10, num_frames, num_channel, num_frames_translation, num_static_translation, num_frames_rotation, offset_frames_translation, offset_static_translation, offset_frames_rotation, offset_data)

animation = []
for k in xrange(num_channel):
   (flag, rx, ry, rz, tx, ty, tz) = struct.unpack_from("<BBBBBBB", data, offset_data + 0x04 + k * 8)

   channel = []
   for j in xrange(num_frames):

                # rotation
                if flag & 0x01:
                    rotation_x = 360.0 * float(ord(data[offset_data + offset_frames_rotation + rx * num_frames + j])) / 255.0
                else:
                    rotation_x = 360.0 * rx / 255.0

                if flag & 0x02:
                    rotation_y = 360.0 * float(ord(data[offset_data + offset_frames_rotation + ry * num_frames + j])) / 255.0
                else:
                    rotation_y = 360.0 * ry / 255.0

                if flag & 0x04:
                    rotation_z = 360.0 * float(ord(data[offset_data + offset_frames_rotation + rz * num_frames + j])) / 255.0
                else:
                    rotation_z = 360.0 * rz / 255.0

                # translation
                if flag & 0x10:
                    (translation_x,) = struct.unpack_from("<h", data, offset_data + offset_frames_translation + tx * num_frames * 2 + j * 2)
                elif tx != 0xFF:
                    (translation_x,) = struct.unpack_from("<h", data, offset_data + offset_static_translation + tx * 2)
else:
   translation_x = 0

                if flag & 0x20:
                    (translation_y,) = struct.unpack_from("<h", data, offset_data + offset_frames_translation + ty * num_frames * 2 + j * 2);
                elif ty != 0xFF:
                    (translation_y,) = struct.unpack_from("<h", data, offset_data + offset_static_translation + ty * 2)
else:
   translation_y = 0

                if flag & 0x40:
                    (translation_z,) = struct.unpack_from("<h", data, offset_data + offset_frames_translation + tz * num_frames * 2 + j * 2)
                elif tz != 0xFF:
                    (translation_z,) = struct.unpack_from("<h", data, offset_data + offset_static_translation + tz * 2)
else:
   translation_z = 0

channel.append(((translation_x, translation_y, translation_z), (rotation_x, rotation_y, rotation_z)))

   animation.append(channel)
animations.append(animation)

    return animations

def loadBcx(name):
    data = loadLzs(name)
    (size, header_offset) = struct.unpack_from("<II", data, 0)
    assert(size == len(data))
    (u1, num_bones, num_parts, num_animations, u2, u3, u4, offset_skeleton) = struct.unpack_from("<HBBB19sHHI", data, header_offset)
    offset_skeleton &= 0x7FFFFFFF
    offset_parts = offset_skeleton + num_bones * 4
    offset_animations = offset_parts + num_parts * 32
    # print "header", (header_offset, u1, num_bones, num_parts, num_animations, u2, u3, u4, offset_skeleton, offset_parts, offset_animations)

    object = {}
    object["model"] = readModel(data, num_bones, offset_skeleton, num_parts, offset_parts)
    object["animations"] = readAnimations(data, num_animations, offset_animations)
    
    return object
    
def loadBsx(name):
    # get compressed file
    data = loadLzs(name)
    (size, header_offset) = struct.unpack_from("<II", data, 0)
    assert(size == len(data))

    # decode individual objects
    object_list = []
    (u1, num_models) = struct.unpack_from("<II", data, header_offset)
    for i in xrange(num_models):
model_offset = header_offset + 0x10 + i * 0x30
(model_id,u2,u3,offset_skeleton,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15,u16,u17,u18,u19,u20,num_bones,u22,u23,u24,u25,u26,u27,u28,u29,u30,u31,u32,num_parts,u34,u35,u36,u37,u38,u39,u40,u41,u42,u43,u44,num_animations) = struct.unpack_from("<HBBHBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", data, model_offset)
# print (model_id,u2,u3,offset_skeleton,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15,u16,u17,u18,u19,u20,num_bones,u22,u23,u24,u25,u26,u27,u28,u29,u30,u31,u32,num_parts,u34,u35,u36,u37,u38,u39,u40,u41,u42,u43,u44,num_animations)
offset_skeleton += model_offset
offset_parts = offset_skeleton + num_bones * 4
offset_animations = offset_parts + num_parts * 32

if model_id == 0x0100:
   object = loadBcx(bcxMap["cloud"])
elif model_id == 0x0302:
   object = loadBcx(bcxMap["ballet"])
elif model_id == 0x0403:
   object = loadBcx(bcxMap["tifa"])
elif model_id == 0x0605:
   object = loadBcx(bcxMap["cid"])
elif model_id == 0x0706:
   object = loadBcx(bcxMap["yufi"])
elif num_parts > 0:
   object = {}
   object["model"] = readModel(data, num_bones, offset_skeleton, num_parts, offset_parts)
   object["animations"] = []
else:
   print "unknown model id: %x" % model_id

object["animations"].extend(readAnimations(data, num_animations, offset_animations))

object_list.append(object)

    return object_list
    
class OpenGLObject:
    def __init__(self, object):
self.object = object
self.time = 0
self.animation = 0
self.draw_normals = False
self.draw_skeleton = True
self.part_map = {}
model = object["model"]
for idx, part in enumerate(model["parts"]):
   self.part_map[part["bone_index"]] = idx

    def nextAnim(self):
self.animation = (self.animation + 1) % len(self.object["animations"])
self.time = 0

    def drawPart(self, part):
glEnable(GL_TEXTURE_2D)

glBegin(GL_TRIANGLES)

for tri in part["tri_color_tex"]:
   for i in xrange(3):
pos = part["vertices"][tri[i][0]]
texcoord = part["texcoords"][tri[i][1]]
col = tri[i][2]

glTexCoord2i(texcoord[0], texcoord[1])
glColor3ub(col[0], col[1], col[2])
glVertex3i(pos[0], pos[1], pos[2])
    
for tri in part["tri_mono_tex"]:
   col = tri[3]
   glColor3ub(col[0], col[1], col[2])
   for i in xrange(3):
pos = part["vertices"][tri[i][0]]
texcoord = part["texcoords"][tri[i][1]]

glTexCoord2i(texcoord[0], texcoord[1])
glColor3ub(col[0], col[1], col[2])
glVertex3i(pos[0], pos[1], pos[2])

glEnd()

glBegin(GL_QUADS)

for quad in part["quad_color_tex"]:
   for i in xrange(4):
pos = part["vertices"][quad[i][0]]
texcoord = part["texcoords"][quad[i][1]]
col = quad[i][2]

glTexCoord2i(texcoord[0], texcoord[1])
glColor3ub(col[0], col[1], col[2])
glVertex3i(pos[0], pos[1], pos[2])

for quad in part["quad_mono_tex"]:
   col = quad[4]
   glColor3ub(col[0], col[1], col[2])
   for i in xrange(4):
pos = part["vertices"][quad[i][0]]
texcoord = part["texcoords"][quad[i][1]]

glTexCoord2i(texcoord[0], texcoord[1])
glVertex3i(pos[0], pos[1], pos[2])

glEnd()

glDisable(GL_TEXTURE_2D)

glBegin(GL_TRIANGLES)

for tri in part["tri_color"]:
   for i in xrange(3):
pos = part["vertices"][tri[i][0]]
col = tri[i][1]

glColor3ub(col[0], col[1], col[2])
glVertex3i(pos[0], pos[1], pos[2])

for tri in part["tri_mono"]:
   col = tri[3]
   glColor3ub(col[0], col[1], col[2])
   for i in xrange(3):
pos = part["vertices"][tri[i][0]]

glVertex3i(pos[0], pos[1], pos[2])

glEnd()

glBegin(GL_QUADS)

for quad in part["quad_color"]:
   for i in xrange(4):
pos = part["vertices"][quad[i][0]]
col = quad[i][1]

glColor3ub(col[0], col[1], col[2])
glVertex3i(pos[0], pos[1], pos[2])

for quad in part["quad_mono"]:
   col = quad[4]
   glColor3ub(col[0], col[1], col[2])
   for i in xrange(4):
pos = part["vertices"][quad[i][0]]

glVertex3i(pos[0], pos[1], pos[2])

glEnd()

# draw normals (I'm not sure about the mono primitives. Maybe they're not lit at all?)
if self.draw_normals:
   global normals
   glBegin(GL_LINES)
   glColor3f(1,1,0)

   for quad in part["quad_mono_tex"]:
for i in xrange(4):
   pos = part["vertices"][quad[i][0]]
   nrm = normals[quad[5]]

   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for tri in part["tri_mono_tex"]:
for i in xrange(3):
   pos = part["vertices"][tri[i][0]]
   nrm = normals[tri[4]]

   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for quad in part["quad_mono"]:
for i in xrange(4):
   pos = part["vertices"][quad[i][0]]
   nrm = normals[quad[5]]

   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for tri in part["tri_mono"]:
for i in xrange(3):
   pos = part["vertices"][tri[i][0]]
   nrm = normals[tri[4]]

   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for quad in part["quad_color_tex"]:
for i in xrange(4):
   pos = part["vertices"][quad[i][0]]
   nrm = normals[quad[i][3]]

   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for tri in part["tri_color_tex"]:
for i in xrange(3):
   pos = part["vertices"][tri[i][0]]
   nrm = normals[tri[i][3]]

   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for quad in part["quad_color"]:
for i in xrange(4):
   pos = part["vertices"][quad[i][0]]
   nrm = normals[quad[i][2]]
   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))

   for tri in part["tri_color"]:
for i in xrange(3):
   pos = part["vertices"][tri[i][0]]
   nrm = normals[tri[i][2]]
   glVertex3i(pos[0], pos[1], pos[2])
   glVertex3i(int(pos[0]+nrm[0]*50), int(pos[1]+nrm[1]*50), int(pos[2]+nrm[2]*50))
   
   glEnd()

    def draw(self):
glMatrixMode(GL_MODELVIEW)

glRotatef(180, 1, 0, 0)

model = self.object["model"]
animation = self.object["animations"][self.animation]
skeleton = model["skeleton"]
self.time = (self.time + 1) % len(animation[0])

parent = [-1]
for idx, bone in enumerate(model["skeleton"]):
   ((translation_x, translation_y, translation_z), (rotation_x, rotation_y, rotation_z)) = animation[idx][self.time]

   while parent[-1] != bone[1]:
parent.pop()
glPopMatrix()
   parent.append(idx)
   glPushMatrix()

   if self.draw_skeleton:
glBegin(GL_LINES)
glColor3f(1.0, 0.0, 1.0)
glVertex3i(0, 0, 0)
glVertex3i(0, 0, bone[0])
glColor3f(1.0, 0.0, 0.0)
glVertex3i(0, 0, 0)
glVertex3i(50, 0, 0)
glColor3f(0.0, 1.0, 0.0)
glVertex3i(0, 0, 0)
glVertex3i(0, 50, 0)
glColor3f(0.0, 0.0, 1.0)
glVertex3i(0, 0, 0)
glVertex3i(0, 0, 50)
glEnd()

   glTranslatef(0, 0, bone[0])    
   glRotatef(rotation_y, 0, 1, 0)
   glRotatef(rotation_x, 1, 0, 0)
   glRotatef(rotation_z, 0, 0, 1)
   glTranslatef(translation_x, translation_y, translation_z)

   if bone[2]:
self.drawPart(model["parts"][self.part_map[idx]])

while parent[-1] != -1:
   parent.pop()
   glPopMatrix()

    
def main(*argv):
    pygame.init()
    
    screen = pygame.display.set_mode((640, 480), HWSURFACE|DOUBLEBUF|OPENGL)

    glViewport(0, 0, 640, 480)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    kFOVy = 0.57735
    kZNear = 1000.0
    kZFar = 3000.0
    aspect = (640.0 / 480.0) * kZNear * kFOVy
    glFrustum(-aspect, aspect, -(kZNear * kFOVy), (kZNear * kFOVy), kZNear, kZFar)

    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LEQUAL)
    glDisable(GL_CULL_FACE)
    glDisable(GL_LIGHTING)
    glDisable(GL_BLEND)
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
        
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)

    filePath = os.path.abspath(argv[0])

    # get list of BCX, which should be in the same dir as the .bsx
    # (This is done this way to support operating sytems with a case-sensitive file systems.)
    global bcxMap
    bcxMap = {}
    fieldInfo = {}
    dir = os.path.dirname(filePath)
    for path in os.listdir(dir):
file = os.path.splitext(path)
        if file[1].lower() == ".bcx":
   bcxMap[file[0].lower()] = os.path.join(dir, path)
elif path.lower() == "field.bin":
   fieldInfo["field.bin"] = os.path.join(dir, path)
elif path.lower() == "field.tdb":
   fieldInfo["field.tdb"] = os.path.join(dir, path)

    # get normals
    fieldBin = loadFieldBin(fieldInfo["field.bin"])
    global normals
    normals = []
    for i in xrange(240):
n = struct.unpack_from("<hhh", fieldBin, 0x3f520 + i * 8)
normals.append(vecNormal(n))

    # load BCX or BSX file
    if os.path.splitext(os.path.basename(filePath))[1].lower() == ".bcx":
data = [loadBcx(filePath)]
    else:
data = loadBsx(filePath)

    clock = pygame.time.Clock()
    key_up = key_down = key_left = key_right = False
    rot_x = 0.0
    rot_y = 0.0
    cur_model = 0
    object = OpenGLObject(data[cur_model])
    wireframe = True
    while True:
for event in pygame.event.get():
   if event.type == QUIT:
exit()
   elif event.type == KEYDOWN:
if event.key == pygame.K_UP:
   key_up = True
elif event.key == pygame.K_DOWN:
   key_down = True
elif event.key == pygame.K_LEFT:
   key_left = True
elif event.key == pygame.K_RIGHT:
   key_right = True
elif event.key == pygame.K_ESCAPE:
   exit()
elif event.key == pygame.K_a:
   object.nextAnim()
elif event.key == pygame.K_n:
   object.draw_normals = not object.draw_normals
elif event.key == pygame.K_s:
   object.draw_skeleton = not object.draw_skeleton
elif event.key == pygame.K_m:
   cur_model = (cur_model + 1) % len(data)
   del object
   object = OpenGLObject(data[cur_model])
elif event.key == pygame.K_w:
   wireframe = not wireframe
   if wireframe:
       glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
   else:
       glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
   elif event.type == KEYUP:
if event.key == pygame.K_UP:
   key_up = False
elif event.key == pygame.K_DOWN:
   key_down = False
elif event.key == pygame.K_LEFT:
   key_left = False
elif event.key == pygame.K_RIGHT:
   key_right = False
   
if key_up:
   rot_x += 1.0
elif key_down:
   rot_x -= 1.0
   
if key_left:
   rot_y += 1.0
elif key_right:
   rot_y -= 1.0
   
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glFrustum(-aspect, aspect, -(kZNear * kFOVy), (kZNear * kFOVy), kZNear, 100000.0)
glTranslatef(0, 0, -2000)
glRotatef(rot_y * 360.0 / 256.0, 0.0, 1.0, 0.0)
glRotatef(rot_x * 360.0 / 256.0, 1.0, 0.0, 0.0)

glMatrixMode(GL_MODELVIEW)
glLoadIdentity()

object.draw()
    
clock.tick(60)
pygame.display.flip()

if __name__ == "__main__":
    main(*sys.argv[1:])
You'll need pygame and the python-opengl bindings. Run with a BCX or BSX file as argument, w toggles wireframe, n toggles normals, s toggles the skeleton, m cycles through models and a cycles through animations.
It should be quite easy to take it from here and make an importer for Blender, but if you're after efficient rendering or animation then Akari's solution in the q-gears source is more efficient.
« Last Edit: 2009-11-08 15:19:44 by Micky »

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #9 on: 2009-10-24 17:30:11 »
2Micky: By the way did you look at KAWAI opcode? it is very much related to models and animations.


Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #10 on: 2009-10-24 19:06:48 »
2Micky: By the way did you look at KAWAI opcode? it is very much related to models and animations.
Not yet. I'll have to add texture support and some basic lighting first. I did check out the stuff from svn, though.

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #11 on: 2009-10-24 19:16:06 »
2Micky: By the way did you look at KAWAI opcode? it is very much related to models and animations.
Not yet. I'll have to add texture support and some basic lighting first. I did check out the stuff from svn, though.

I understand texture completly, so look into svn. just need to fing BLINKing routine.

I update SVN daily so stay tuned. Just got to function that calculates lighting.

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #12 on: 2009-10-26 09:45:30 »
After a bit of more exploration it looks like the unused byte after each vertex colour could be the normal index. The index has similar values as the vertex index (i.e. if two vertices have the same indices, the corresponding unused values are the same as well), but the normal table doesn't seem to be in the model. Maybe it is shared across all models and stored in the executable?
Another bit: The index goes from 0 to 240 for most BCX. Only Red 13 has a few less.
It's of course possible as well that the value directly encodes the value, and is split into two angle values.

You are right! This is index in normal's table. It's stored at 800df520 address (in executable, right after cos/sin table). It used to calculate final vertex color. And thgere is only 240 values (0x00-0xEF).
« Last Edit: 2009-10-26 09:47:21 by Akari »

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #13 on: 2009-10-26 21:41:33 »
You are right! This is index in normal's table. It's stored at 800df520 address (in executable, right after cos/sin table). It used to calculate final vertex color. And thgere is only 240 values (0x00-0xEF).
I've seen it in emulator memory, though it doesn't seem to be in the main executable. I just graphed it in Open Office and it looks very regular. Maybe a subdivision scheme?
« Last Edit: 2009-10-26 22:17:16 by Micky »

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #14 on: 2009-10-27 12:17:49 »
You are right! This is index in normal's table. It's stored at 800df520 address (in executable, right after cos/sin table). It used to calculate final vertex color. And thgere is only 240 values (0x00-0xEF).
I've seen it in emulator memory, though it doesn't seem to be in the main executable. I just graphed it in Open Office and it looks very regular. Maybe a subdivision scheme?

I'll trace loading later. I finished with model initialization, now switch to ingame animation and model state update.

Micky

  • *
  • Posts: 300
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #15 on: 2009-11-08 13:27:42 »
I found it in field.bin, at offset 0x3f520. I added it to the discussion about field.bin in the Q-Gears section. (Would it make sense to move that thread here?)

Update: I updated the python code above to read in the normals from field.bin. It assumes that the normal table is at a fixed address, so it may break if the file is different in another build. I'm using NTSC-U/C.
I didn't find a test model that uses mono or mono+tex, so I just assumed in those cases it's the forth component of the colour as well.
« Last Edit: 2009-11-08 15:26:05 by Micky »

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #16 on: 2009-11-17 15:24:20 »
Almost finish new field model exporter. Now it uses all the files that contain model data (BSX/BCX/TDB/DAT) ^_^
And correctly assign all textures (including eye and mouth)

Akari

  • *
  • Posts: 766
    • View Profile
Re: [FF7, Field, PSX, Animation] BCX/BSX file formats?
« Reply #17 on: 2009-11-23 20:20:49 »
I upload lastest exporter to svn. Almost all models exported correctly. And there i config file where you can set what to export.
It also can be used as viewer.