2017-09-08 09:24:16 -07:00
//
// nsbtx.js
//--------------------
// Reads NSBTX files (or TEX0 sections) and provides canvases containing decoded texture data.
// by RHY3756547
//
// includes: gl-matrix.js (glMatrix 2.0)
// /formats/nitro.js
//
window . nsbtx = function ( input , tex0 ) {
var texDataSize , texInfoOff , texOffset , compTexSize , compTexInfoOff ,
compTexOffset , compTexInfoDataOff /*wtf*/ , palSize , palInfoOff ,
palOffset , mainOff
var textureInfo , paletteInfo , palData , texData , compData , compInfoData , colourBuffer
var thisObj = this ;
var bitDepths = [ 0 , 8 , 2 , 4 , 8 , 2 , 8 , 16 ]
if ( input != null ) {
load ( input , tex0 ) ;
}
this . load = load ;
this . readTexWithPal = readTexWithPal ;
this . cache = { } ; //textures for btx are cached in this object.
this . scopeEval = function ( code ) { return eval ( code ) } //for debug purposes
function load ( input , tex0 ) {
colourBuffer = new Uint32Array ( 4 ) ;
var view = new DataView ( input ) ;
var header = null ;
var offset = 0 ;
if ( ! tex0 ) { //nitro 3d header
header = nitro . readHeader ( view ) ;
if ( header . stamp != "BTX0" ) throw "nsbtx invalid. Expected BTX0, found " + header . stamp ;
if ( header . numSections > 1 ) throw "NSBTX invalid. Too many sections - should only have one." ;
offset = header . sectionOffsets [ 0 ] ;
}
mainOff = offset ;
var stamp = readChar ( view , offset + 0x0 ) + readChar ( view , offset + 0x1 ) + readChar ( view , offset + 0x2 ) + readChar ( view , offset + 0x3 ) ;
if ( stamp != "TEX0" ) throw "NSBTX invalid. Expected TEX0, found " + stamp ;
var size = view . getUint32 ( offset + 0x04 , true ) ;
texDataSize = view . getUint16 ( offset + 0x0C , true ) << 3 ;
texInfoOff = view . getUint16 ( offset + 0x0E , true ) ;
texOffset = view . getUint16 ( offset + 0x14 , true ) ;
compTexSize = view . getUint16 ( offset + 0x1C , true ) << 3 ;
compTexInfoOff = view . getUint16 ( offset + 0x1E , true ) ;
compTexOffset = view . getUint32 ( offset + 0x24 , true ) ;
compTexInfoDataOff = view . getUint32 ( offset + 0x28 , true ) ;
palSize = view . getUint32 ( offset + 0x30 , true ) << 3 ;
palInfoOff = view . getUint32 ( offset + 0x34 , true ) ;
palOffset = view . getUint32 ( offset + 0x38 , true ) ;
//read palletes, then textures.
var po = mainOff + palOffset ;
palData = input . slice ( po , po + palSize ) ;
var to = mainOff + texOffset ;
texData = input . slice ( to , to + texDataSize ) ;
var co = mainOff + compTexOffset ;
compData = input . slice ( co , co + compTexSize ) ; //pixel information for compression. 2bpp, 16 pixels, so per 4x4 block takes up 4 bytes
var cio = mainOff + compTexInfoDataOff ;
compInfoData = input . slice ( cio , cio + compTexSize / 2 ) ; //each 4x4 block has a 16bit information uint. 2 bytes per block, thus half the size of above.
paletteInfo = nitro . read3dInfo ( view , mainOff + palInfoOff , palInfoHandler ) ;
textureInfo = nitro . read3dInfo ( view , mainOff + texInfoOff , texInfoHandler ) ;
2019-06-09 15:50:55 -07:00
buildNameToIndex ( paletteInfo ) ;
buildNameToIndex ( textureInfo ) ;
2017-09-08 09:24:16 -07:00
thisObj . paletteInfo = paletteInfo ;
thisObj . textureInfo = textureInfo ;
}
2019-06-09 15:50:55 -07:00
function buildNameToIndex ( info ) {
var nameToIndex = { } ;
for ( var i = 0 ; i < info . names . length ; i ++ ) {
nameToIndex [ "$" + info . names [ i ] ] = i ;
}
info . nameToIndex = nameToIndex ;
}
2017-09-08 09:24:16 -07:00
function readTexWithPal ( textureId , palId ) {
var tex = textureInfo . objectData [ textureId ] ;
var pal = paletteInfo . objectData [ palId ] ;
var format = tex . format ;
var trans = tex . pal0trans ;
if ( format == 5 ) return readCompressedTex ( tex , pal ) ; //compressed 4x4 texture, different processing entirely
var off = tex . texOffset ;
var palView = new DataView ( palData ) ;
var texView = new DataView ( texData ) ;
var palOff = pal . palOffset ;
var canvas = document . createElement ( "canvas" ) ;
canvas . width = tex . width ;
canvas . height = tex . height ;
var ctx = canvas . getContext ( "2d" ) ;
var img = ctx . getImageData ( 0 , 0 , tex . width , tex . height ) ;
var total = tex . width * tex . height ;
var databuf ;
for ( var i = 0 ; i < total ; i ++ ) {
var col ;
if ( format == 1 ) { //A3I5 encoding. 3 bits alpha 5 bits pal index
var dat = texView . getUint8 ( off ++ )
col = readPalColour ( palView , palOff , dat & 31 , trans ) ;
col [ 3 ] = ( dat >> 5 ) * ( 255 / 7 ) ;
2019-06-09 15:50:55 -07:00
premultiply ( col ) ;
2017-09-08 09:24:16 -07:00
} else if ( format == 2 ) { //2 bit pal
if ( i % 4 == 0 ) databuf = texView . getUint8 ( off ++ ) ;
col = readPalColour ( palView , palOff , ( databuf >> ( ( i % 4 ) * 2 ) ) & 3 , trans )
} else if ( format == 3 ) { //4 bit pal
if ( i % 2 == 0 ) {
databuf = texView . getUint8 ( off ++ ) ;
col = readPalColour ( palView , palOff , databuf & 15 , trans )
} else {
col = readPalColour ( palView , palOff , databuf >> 4 , trans )
}
} else if ( format == 4 ) { //8 bit pal
col = readPalColour ( palView , palOff , texView . getUint8 ( off ++ ) , trans )
} else if ( format == 6 ) { //A5I3 encoding. 5 bits alpha 3 bits pal index
var dat = texView . getUint8 ( off ++ )
col = readPalColour ( palView , palOff , dat & 7 , trans ) ;
col [ 3 ] = ( dat >> 3 ) * ( 255 / 31 ) ;
2019-06-09 15:50:55 -07:00
premultiply ( col ) ;
2017-09-08 09:24:16 -07:00
} else if ( format == 7 ) { //raw color data
col = texView . getUint16 ( off , true ) ;
colourBuffer [ 0 ] = Math . round ( ( ( col & 31 ) / 31 ) * 255 )
colourBuffer [ 1 ] = Math . round ( ( ( ( col >> 5 ) & 31 ) / 31 ) * 255 )
colourBuffer [ 2 ] = Math . round ( ( ( ( col >> 10 ) & 31 ) / 31 ) * 255 )
colourBuffer [ 3 ] = Math . round ( ( col >> 15 ) * 255 ) ;
col = colourBuffer ;
2019-06-09 15:50:55 -07:00
premultiply ( col ) ;
2017-09-08 09:24:16 -07:00
off += 2 ;
} else {
console . log ( "texture format is none, ignoring" )
return canvas ;
}
img . data . set ( col , i * 4 ) ;
}
ctx . putImageData ( img , 0 , 0 )
return canvas ;
}
2019-06-09 15:50:55 -07:00
function premultiply ( col ) {
col [ 0 ] *= col [ 3 ] / 255 ;
col [ 1 ] *= col [ 3 ] / 255 ;
col [ 2 ] *= col [ 3 ] / 255 ;
}
2017-09-08 09:24:16 -07:00
function readCompressedTex ( tex , pal ) { //format 5, 4x4 texels. I'll keep this well documented so it's easy to understand.
var off = tex . texOffset ;
var texView = new DataView ( compData ) ; //real texture data - 32 bits per 4x4 block (one byte per 4px horizontal line, each descending 1px)
var compView = new DataView ( compInfoData ) ; //view into compression info - informs of pallete and parameters.
var palView = new DataView ( palData ) ; //view into the texture pallete
var compOff = off / 2 ; //info is 2 bytes per block, so the offset is half that of the tex offset.
var palOff = pal . palOffset ;
var transColor = new Uint8Array ( [ 0 , 0 , 0 , 0 ] ) ; //transparent black
var canvas = document . createElement ( "canvas" ) ;
canvas . width = tex . width ;
canvas . height = tex . height ;
var ctx = canvas . getContext ( "2d" ) ;
var img = ctx . getImageData ( 0 , 0 , tex . width , tex . height ) ;
var w = tex . width >> 2 ; //iterate over blocks, block w and h is /4.
var h = tex . height >> 2 ;
for ( var y = 0 ; y < h ; y ++ ) {
for ( var x = 0 ; x < w ; x ++ ) {
//inside block
var bInfo = compView . getUint16 ( compOff , true ) ; //block info
var addr = ( bInfo & 0x3fff ) ; //offset to relevant pallete
var mode = ( ( bInfo >> 14 ) & 3 ) ;
var finalPo = palOff + addr * 4 ;
var imgoff = x * 4 + ( y * w * 16 ) ;
for ( var iy = 0 ; iy < 4 ; iy ++ ) {
var dat = texView . getUint8 ( off ++ ) ;
for ( var ix = 0 ; ix < 4 ; ix ++ ) { //iterate over horiz lines
var part = ( dat >> ( ix * 2 ) ) & 3 ;
var col ;
switch ( mode ) {
case 0 : //value 3 is transparent, otherwise pal colour
if ( part == 3 ) col = transColor ;
else col = readPalColour ( palView , finalPo , part ) ;
break ;
case 1 : //average mode - colour 2 is average of 1st two, 3 is transparent. 0&1 are normal.
if ( part == 3 ) col = transColor ;
else if ( part == 2 ) col = readFractionalPal ( palView , finalPo , 0.5 ) ;
else col = readPalColour ( palView , finalPo , part ) ;
break ;
case 2 : //pal colour
col = readPalColour ( palView , finalPo , part ) ;
break ;
case 3 : //5/8 3/8 mode - colour 2 is 5/8 of col0 plus 3/8 of col1, 3 is 3/8 of col0 plus 5/8 of col1. 0&1 are normal.
if ( part == 3 ) col = readFractionalPal ( palView , finalPo , 3 / 8 ) ;
else if ( part == 2 ) col = readFractionalPal ( palView , finalPo , 5 / 8 ) ;
else col = readPalColour ( palView , finalPo , part ) ;
break ;
}
img . data . set ( col , ( imgoff ++ ) * 4 )
}
imgoff += tex . width - 4 ;
}
compOff += 2 ; //align off to next block
}
}
ctx . putImageData ( img , 0 , 0 )
return canvas ;
}
function readPalColour ( view , palOff , ind , pal0trans ) {
var col = view . getUint16 ( palOff + ind * 2 , true ) ;
var f = 255 / 31 ;
colourBuffer [ 0 ] = Math . round ( ( col & 31 ) * f )
colourBuffer [ 1 ] = Math . round ( ( ( col >> 5 ) & 31 ) * f )
colourBuffer [ 2 ] = Math . round ( ( ( col >> 10 ) & 31 ) * f )
colourBuffer [ 3 ] = ( pal0trans && ind == 0 ) ? 0 : 255 ;
return colourBuffer ;
}
function readFractionalPal ( view , palOff , i ) {
var col = view . getUint16 ( palOff , true ) ;
var col2 = view . getUint16 ( palOff + 2 , true ) ;
var ni = 1 - i ;
var f = 255 / 31 ;
colourBuffer [ 0 ] = Math . round ( ( col & 31 ) * f * i + ( col2 & 31 ) * f * ni )
colourBuffer [ 1 ] = Math . round ( ( ( col >> 5 ) & 31 ) * f * i + ( ( col2 >> 5 ) & 31 ) * f * ni )
colourBuffer [ 2 ] = Math . round ( ( ( col >> 10 ) & 31 ) * f * i + ( ( col2 >> 10 ) & 31 ) * f * ni )
colourBuffer [ 3 ] = 255 ;
return colourBuffer ;
}
function palInfoHandler ( view , offset ) {
var palOffset = view . getUint16 ( offset , true ) << 3 ;
var unknown = view . getUint16 ( offset + 2 , true ) ;
return {
palOffset : palOffset ,
unknown : unknown ,
nextoff : offset + 4
}
}
function texInfoHandler ( view , offset ) {
var texOffset = view . getUint16 ( offset , true ) << 3 ;
var flags = view . getUint16 ( offset + 2 , true ) ;
var width2 = view . getUint8 ( offset + 4 , true ) ;
var unknown = view . getUint8 ( offset + 5 , true ) ;
var height2 = view . getUint8 ( offset + 6 , true ) ;
var unknown2 = view . getUint8 ( offset + 7 , true ) ;
return {
texOffset : texOffset ,
pal0trans : ( flags >> 13 ) & 1 , //two top flags are texture matrix modes. not sure if it really matters (except for nsbta animation maybe, but 0 = no transform and things that have tex animations are set to 0 anyways).
format : ( ( flags >> 10 ) & 7 ) ,
height : 8 << ( ( flags >> 7 ) & 7 ) ,
width : 8 << ( ( flags >> 4 ) & 7 ) ,
repeatX : flags & 1 ,
repeatY : ( flags >> 1 ) & 1 ,
flipX : ( flags >> 2 ) & 1 ,
flipY : ( flags >> 3 ) & 1 ,
unkWidth : width2 ,
unk1 : unknown ,
unkHeight : height2 ,
unk2 : unknown2 ,
nextoff : offset + 8
}
}
function readChar ( view , offset ) {
return String . fromCharCode ( view . getUint8 ( offset ) ) ;
}
}