Skip to content

Commit

Permalink
OBJ: added support for BSTriShape geometry and vertex colors, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
fo76utils committed Aug 9, 2024
1 parent b970169 commit 292cf55
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 76 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Vertex selection has been implemented for Starfield, and the maximum number of shapes supported by vertex selection has been increased from 256 to 32768 for all games.
* Fixed the 'Add Tangent Spaces and Update' spell for Starfield.
* Importing OBJ files to Skyrim SE, Fallout 4 and Fallout 76 models now creates BSTriShape geometry instead of NiTriShape.
* Added support for vertex colors in exported and imported OBJ files (BSTriShape only).
* Fixes to OBJ import.
* Disabled some of the spells for newer games where they are not implemented or should not be applicable (e.g. Fallout 4 and newer do not use triangle strips).

Expand Down
242 changes: 166 additions & 76 deletions src/lib/importex/obj.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "gl/gltex.h"
#include "gl/glscene.h"
#include "model/nifmodel.h"
#include "nifskope.h"
#include "spells/tangentspace.h"
#include "qtcompat.h"

Expand Down Expand Up @@ -141,6 +142,7 @@ static void writeData( const NifModel * nif, const QModelIndex & iData, QTextStr
QVector<Vector3> verts;
QVector<Vector2> coords;
QVector<Vector3> norms;
QVector<ByteColor4> colors;

auto numVerts = nif->get<int>( iData, "Num Vertices" );
for ( int i = 0; i < numVerts; i++ ) {
Expand All @@ -149,11 +151,20 @@ static void writeData( const NifModel * nif, const QModelIndex & iData, QTextStr
verts += nif->get<Vector3>( idx, "Vertex" );
coords += nif->get<HalfVector2>( idx, "UV" );
norms += nif->get<ByteVector3>( idx, "Normal" );
QModelIndex iColors = nif->getIndex( idx, "Vertex Colors" );
if ( iColors.isValid() )
colors += nif->get<ByteColor4>( iColors );
}

for ( Vector3 & v : verts ) {
v = t * v;
obj << "v " << qSetRealNumberPrecision( 17 ) << v[0] << " " << v[1] << " " << v[2] << "\r\n";
obj << "v " << qSetRealNumberPrecision( 17 ) << v[0] << " " << v[1] << " " << v[2];
qsizetype i = qsizetype( &v - verts.constData() );
if ( i < colors.size() ) {
ByteColor4 c = colors.at( i );
obj << qSetRealNumberPrecision( 5 ) << " " << c[0] << " " << c[1] << " " << c[2];
}
obj << "\r\n";
}

for ( Vector2 & c : coords ) {
Expand Down Expand Up @@ -605,8 +616,7 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
if ( iBlock.isValid() && nif->blockInherits( iBlock, "NiNode" ) ) {
iNode = iBlock;
} else if ( iBlock.isValid()
&& (nif->itemName( iBlock ) == "NiTriShape"
|| (collision && nif->blockInherits( iBlock, "BSTriShape" ))) ) {
&& (nif->itemName( iBlock ) == "NiTriShape" || nif->blockInherits( iBlock, "BSTriShape" )) ) {
iShape = iBlock;
//Find parent of NiTriShape
int par_num = nif->getParent( nif->getBlockNumber( iBlock ) );
Expand All @@ -629,7 +639,7 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )

if ( type == "NiMaterialProperty" ) {
iMaterial = temp;
} else if ( type == "NiTriShapeData" ) {
} else if ( type == "NiTriShapeData" || nif->blockInherits( temp, "BSTriShape" ) ) {
iData = temp;
} else if ( (type == "NiTexturingProperty") || (type == "NiTextureProperty") ) {
iTexProp = temp;
Expand All @@ -655,12 +665,12 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
if ( !collision ) {
if ( iNode.isValid() ) {
if ( iShape.isValid() ) {
question = tr( "NiTriShape selected. The first imported mesh will replace the selected one." );
question = tr( "Shape selected. The first imported mesh will replace the selected one." );
} else {
question = tr( "NiNode selected. Meshes will be attached to the selected node." );
}
} else {
question = tr( "No NiNode or NiTriShape selected. Meshes will be imported to the root of the file." );
question = tr( "No NiNode or shape selected. Meshes will be imported to the root of the file." );
}
} else {
if ( iNode.isValid() || iShape.isValid() ) {
Expand Down Expand Up @@ -698,6 +708,7 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
QVector<Vector3> overts;
QVector<Vector3> onorms;
QVector<Vector2> otexco;
QVector<ByteColor4> ocolors;
QMap<QString, QVector<ObjFace> *> ofaces;
QMap<QString, ObjMaterial> omaterials;

Expand Down Expand Up @@ -727,6 +738,14 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
}
} else if ( t.value( 0 ) == "v" ) {
overts.append( Vector3( t.value( 1 ).toDouble(), t.value( 2 ).toDouble(), t.value( 3 ).toDouble() ) );
ByteColor4 c;
if ( t.size() == 7 ) {
// X, Y, Z, R, G, B format
c[0] = t.value( 4 ).toFloat();
c[1] = t.value( 5 ).toFloat();
c[2] = t.value( 6 ).toFloat();
}
ocolors.append( c );
} else if ( t.value( 0 ) == "vt" ) {
otexco.append( Vector2( t.value( 1 ).toDouble(), 1.0 - t.value( 2 ).toDouble() ) );
} else if ( t.value( 0 ) == "vn" ) {
Expand Down Expand Up @@ -778,8 +797,7 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
nif->set<QString>( iNode, "Name", "Scene Root" );
}

// create a NiTriShape foreach material in the object
// TODO: create BSTriShape for BS version >= 100
// create a NiTriShape or BSTriShape for each material in the object
int shapecount = 0;
bool first_tri_shape = true;
QMapIterator<QString, QVector<ObjFace> *> it( ofaces );
Expand All @@ -792,19 +810,60 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
if ( !it.value()->count() )
continue;

QVector<Vector3> verts;
QVector<Vector3> norms;
QVector<Vector2> texco;
QVector<ByteColor4> colors;
QVector<Triangle> triangles;

QVector<ObjPoint> points;
bool haveVertexColors = false;

foreach ( ObjFace oface, *( it.value() ) ) {
Triangle tri;

for ( int t = 0; t < 3; t++ ) {
ObjPoint p = oface.p[t];
int ix;

for ( ix = 0; ix < points.count(); ix++ ) {
if ( points[ix] == p )
break;
}

if ( ix == points.count() ) {
points.append( p );
verts.append( overts.value( p.v ) );
norms.append( onorms.value( p.n ) );
texco.append( otexco.value( p.t ) );
ByteColor4 c = ocolors.value( p.v );
colors.append( c );
haveVertexColors = haveVertexColors || ( std::uint32_t( c ) != 0xFFFFFFFFU );
}

tri[t] = ix;
}

triangles.append( tri );
}

if ( !collision ) {
//If we are on the first shape, and one was selected in the 3D view, use the existing one
bool newiShape = false;

if ( iShape.isValid() == false || first_tri_shape == false ) {
iShape = nif->insertNiBlock( "NiTriShape" );
iShape = nif->insertNiBlock( nif->getBSVersion() < 100 ? "NiTriShape" : "BSTriShape" );
newiShape = true;
}

if ( newiShape ) {
// don't change a name what already exists; // don't add duplicates
nif->set<QString>( iShape, "Name", QString( "%1:%2" ).arg( nif->get<QString>( iNode, "Name" ) ).arg( shapecount++ ) );
addLink( nif, iNode, "Children", nif->getBlockNumber( iShape ) );
if ( nif->getBSVersion() >= 100 ) {
iData = iShape;
nif->set<quint32>( iShape, "Flags", 14 );
}
}

if ( !omaterials.contains( it.key() ) ) {
Expand Down Expand Up @@ -840,9 +899,27 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
if ( newiMaterial ) // don't add property that is already there
addLink( nif, iShape, "Properties", nif->getBlockNumber( iMaterial ) );
} else {
shaderProp = nif->insertNiBlock( "BSLightingShaderProperty" );
nif->setLink( iShape, "Shader Property", nif->getBlockNumber( shaderProp ) );
shaderProp = nif->getIndex( shaderProp, "Shader Property Data" );
if ( !newiShape )
shaderProp = nif->getBlockIndex( nif->getLink( iShape, "Shader Property" ) );
if ( shaderProp.isValid() ) {
shaderProp = nif->getIndex( shaderProp, "Shader Property Data" );
} else {
shaderProp = nif->insertNiBlock( "BSLightingShaderProperty" );
nif->setLink( iShape, "Shader Property", nif->getBlockNumber( shaderProp ) );
shaderProp = nif->getIndex( shaderProp, "Shader Property Data" );
if ( nif->getBSVersion() >= 151 ) {
nif->set<quint32>( shaderProp, "Num SF1", quint32( haveVertexColors ) + 4 );
QModelIndex iSF1 = nif->getIndex( shaderProp, "SF1" );
nif->updateArraySize( iSF1 );
nif->set<quint32>( QModelIndex_child( iSF1, 0 ), ShaderFlags::CAST_SHADOWS );
nif->set<quint32>( QModelIndex_child( iSF1, 1 ), ShaderFlags::ZBUFFER_TEST );
nif->set<quint32>( QModelIndex_child( iSF1, 2 ), ShaderFlags::ZBUFFER_WRITE );
nif->set<quint32>( QModelIndex_child( iSF1, 3 ), ShaderFlags::PBR );
if ( haveVertexColors )
nif->set<quint32>( QModelIndex_child( iSF1, 4 ), ShaderFlags::VERTEXCOLORS );
}
}

if ( nif->getBSVersion() < 130 )
nif->set<float>( shaderProp, "Glossiness", mtl.Ns );
else
Expand Down Expand Up @@ -921,82 +998,90 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
}
}

if ( iData.isValid() == false || first_tri_shape == false ) {
iData = nif->insertNiBlock( "NiTriShapeData" );
}

nif->setLink( iShape, "Data", nif->getBlockNumber( iData ) );

QVector<Vector3> verts;
QVector<Vector3> norms;
QVector<Vector2> texco;
QVector<Triangle> triangles;

QVector<ObjPoint> points;

foreach ( ObjFace oface, *( it.value() ) ) {
Triangle tri;
if ( nif->getBSVersion() < 100 ) {
// NiTriShape
if ( iData.isValid() == false || first_tri_shape == false ) {
iData = nif->insertNiBlock( "NiTriShapeData" );
}

for ( int t = 0; t < 3; t++ ) {
ObjPoint p = oface.p[t];
int ix;
nif->setLink( iShape, "Data", nif->getBlockNumber( iData ) );

nif->set<int>( iData, "Has Vertices", 1 );
nif->set<int>( iData, "Num Vertices", verts.count() );
nif->updateArraySize( iData, "Vertices" );
nif->setArray<Vector3>( iData, "Vertices", verts );
nif->set<int>( iData, "Has Normals", 1 );
nif->updateArraySize( iData, "Normals" );
nif->setArray<Vector3>( iData, "Normals", norms );
// the following fields are version dependent
for ( auto i = nif->getItem( iData, "Has UV" ); i; i = nullptr )
nif->set<int>( i, 1 );
for ( auto i = nif->getItem( iData, "Num UV Sets" ); i; i = nullptr )
nif->set<int>( i, 1 );
for ( auto i = nif->getItem( iData, "Data Flags" ); i; i = nullptr )
nif->set<int>( i, 4097 );
for ( auto i = nif->getItem( iData, "BS Data Flags" ); i; i = nullptr )
nif->set<int>( i, 4097 );

if ( nif->getBSVersion() > 34 ) {
nif->set<int>( iData, "Has Vertex Colors", 1 );
nif->updateArraySize( iData, "Vertex Colors" );
}

for ( ix = 0; ix < points.count(); ix++ ) {
if ( points[ix] == p )
break;
}
QModelIndex iTexCo = nif->getIndex( iData, "UV Sets" );
nif->updateArraySize( iTexCo );
nif->updateArraySize( QModelIndex_child( iTexCo, 0, 0 ) );
nif->setArray<Vector2>( QModelIndex_child( iTexCo, 0, 0 ), texco );

if ( ix == points.count() ) {
points.append( p );
verts.append( overts.value( p.v ) );
norms.append( onorms.value( p.n ) );
texco.append( otexco.value( p.t ) );
}
nif->set<int>( iData, "Has Triangles", 1 );
nif->set<int>( iData, "Num Triangles", triangles.count() );
nif->set<int>( iData, "Num Triangle Points", triangles.count() * 3 );
nif->updateArraySize( iData, "Triangles" );
nif->setArray<Triangle>( iData, "Triangles", triangles );

tri[t] = ix;
nif->set<int>( iData, "Consistency Flags", 0x4000 ); // CT_STATIC
} else {
// BSTriShape
// 28 or 32 bytes: position as floats (16), UV (4), normal (4), tangent (4), color (4)
// TODO: use half precision?
std::uint64_t vertexDesc = 0x0001B00000650407ULL;
if ( nif->getBSVersion() >= 130 )
vertexDesc = vertexDesc | 0x0040000000000000ULL; // full precision flag for Fallout 4 and 76
if ( haveVertexColors )
vertexDesc = vertexDesc + 0x0002000007000001ULL; // enable vertex colors
nif->set<BSVertexDesc>( iShape, "Vertex Desc", vertexDesc );
nif->set<quint32>( iShape, "Num Triangles", quint32( triangles.size() ) );
nif->set<quint32>( iShape, "Num Vertices", quint32( verts.size() ) );
nif->set<quint32>( iShape, "Data Size",
size_t( verts.size() ) * ( !haveVertexColors ? 28 : 32 )
+ size_t( triangles.size() ) * 6 );

nif->setState( BaseModel::Processing );

QModelIndex iVerts = nif->getIndex( iShape, "Vertex Data" );
nif->updateArraySize( iVerts );
for ( qsizetype i = 0; i < verts.size(); i++ ) {
QModelIndex iVertex = QModelIndex_child( iVerts, int( i ) );
if ( !iVertex.isValid() )
continue;
nif->set<Vector3>( iVertex, "Vertex", verts.at( i ) );
nif->set<HalfVector2>( iVertex, "UV", texco.at( i ) );
nif->set<ByteVector3>( iVertex, "Normal", ByteVector3( norms.at( i ) ) );
QModelIndex j = nif->getIndex( iVertex, "Vertex Colors" );
if ( j.isValid() )
nif->set<ByteColor4>( j, colors.at( i ) );
}

triangles.append( tri );
}
QModelIndex iTriangles = nif->getIndex( iShape, "Triangles" );
nif->updateArraySize( iTriangles );
nif->setArray<Triangle>( iTriangles, triangles );

nif->set<int>( iData, "Has Vertices", 1 );
nif->set<int>( iData, "Num Vertices", verts.count() );
nif->updateArraySize( iData, "Vertices" );
nif->setArray<Vector3>( iData, "Vertices", verts );
nif->set<int>( iData, "Has Normals", 1 );
nif->updateArraySize( iData, "Normals" );
nif->setArray<Vector3>( iData, "Normals", norms );
// the following fields are version dependent
for ( auto i = nif->getItem( iData, "Has UV" ); i; i = nullptr )
nif->set<int>( i, 1 );
for ( auto i = nif->getItem( iData, "Num UV Sets" ); i; i = nullptr )
nif->set<int>( i, 1 );
for ( auto i = nif->getItem( iData, "Data Flags" ); i; i = nullptr )
nif->set<int>( i, 4097 );
for ( auto i = nif->getItem( iData, "BS Data Flags" ); i; i = nullptr )
nif->set<int>( i, 4097 );

if ( nif->getBSVersion() > 34 ) {
nif->set<int>( iData, "Has Vertex Colors", 1 );
nif->updateArraySize( iData, "Vertex Colors" );
nif->restoreState();
}

QModelIndex iTexCo = nif->getIndex( iData, "UV Sets" );
nif->updateArraySize( iTexCo );
nif->updateArraySize( QModelIndex_child( iTexCo, 0, 0 ) );
nif->setArray<Vector2>( QModelIndex_child( iTexCo, 0, 0 ), texco );

nif->set<int>( iData, "Has Triangles", 1 );
nif->set<int>( iData, "Num Triangles", triangles.count() );
nif->set<int>( iData, "Num Triangle Points", triangles.count() * 3 );
nif->updateArraySize( iData, "Triangles" );
nif->setArray<Triangle>( iData, "Triangles", triangles );

// "find me a center": see nif.xml for details
BoundSphere bounds( verts, true );
bounds.update( nif, iData );

nif->set<int>( iData, "Consistency Flags", 0x4000 ); // CT_STATIC
} else if ( nif->getBSVersion() > 0 ) {
// create experimental havok collision mesh
QVector<Vector3> verts;
Expand Down Expand Up @@ -1117,6 +1202,11 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision )
settings.endGroup(); // Import-Export

nif->reset();

// center view
NifSkope * w = dynamic_cast< NifSkope * >( nif->getWindow() );
if ( w )
w->on_aViewCenter_triggered();
}

void importObj( NifModel* nif, const QModelIndex& index )
Expand Down

0 comments on commit 292cf55

Please sign in to comment.