Building tangent vectors Topic is solved

Discussion area about developing with Ogre-Next (2.1, 2.2 and beyond)


jwwalker
Goblin
Posts: 291
Joined: Thu Aug 12, 2021 10:06 pm
Location: San Diego, CA, USA
x 21

Building tangent vectors

Post by jwwalker »

I was getting an exception saying that I can't use a normal map without having tangent vectors, and I found some discussion about that in the long "moving Stunt Rally" thread. So I wrote some code that tries to detect when tangent vectors are missing, and build them if needed, but it doesn't seem to work...

Code: Select all

static Ogre::VertexArrayObject* GetFirstVAO( Ogre::MeshPtr inMesh )
{
	Ogre::VertexArrayObject* resultVAO = nullptr;
	if (inMesh->getNumSubMeshes() > 0)
	{
		Ogre::SubMesh* firstSub = inMesh->getSubMesh( 0 );
		Ogre::VertexArrayObjectArray& vaos( firstSub->mVao[ Ogre::VpNormal ] );
		if (not vaos.empty())
		{
			resultVAO = vaos[0];
		}
	}
	
	return resultVAO;
}

static bool HasTangents( Ogre::VertexArrayObject* inVAO )
{
	bool haveTangents = true;

	size_t ignoredIndex, ignoredOffset;
	if ( (inVAO != nullptr) and
		(nullptr == inVAO->findBySemantic( Ogre::VES_TANGENT,
		ignoredIndex, ignoredOffset )) )
	{
		haveTangents = false;
	}
	
	return haveTangents;
}

static bool CanBuildTangents( Ogre::VertexArrayObject* inVAO )
{
	size_t ignoredIndex, ignoredOffset;
	
	const Ogre::VertexElement2* norm = inVAO->findBySemantic( Ogre::VES_NORMAL,
		ignoredIndex, ignoredOffset );
	
	const Ogre::VertexElement2* uv = inVAO->findBySemantic( Ogre::VES_TEXTURE_COORDINATES,
		ignoredIndex, ignoredOffset );
		
	return (norm != nullptr) and (uv != nullptr);
}

static void AddTangentVectors( Ogre::MeshPtr inMesh )
{
	// Inconveniently, we must convert to a temporary v1 mesh in order
	// to create tangents.
	Ogre::v1::MeshPtr m1 = Ogre::v1::MeshManager::getSingleton().create(
		"TempV1ForTangents", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME );
	JWAssert( m1.get() != nullptr );
	m1->setVertexBufferPolicy( Ogre::v1::HardwareBuffer::HBU_STATIC, false );
	m1->setIndexBufferPolicy( Ogre::v1::HardwareBuffer::HBU_STATIC, false );
	m1->importV2( inMesh.get() );
	m1->buildTangentVectors();
	inMesh->importV1( m1.get(), false, false, false );
	Ogre::v1::MeshManager::getSingleton().remove( "TempV1ForTangents" );
}

That is, if HasTangents( GetFirstVAO( mesh ) ) returns false and CanBuildTangents( GetFirstVAO( mesh ) ) returns true, I call AddTangentVectors( mesh ). And in that case, I then call HasTangents( GetFirstVAO( mesh ) ) again as a check, and it STILL returns false. Anyone see what I'm doing wrong?

User avatar
dark_sylinc
OGRE Team Member
OGRE Team Member
Posts: 5509
Joined: Sat Jul 21, 2007 4:55 pm
Location: Buenos Aires, Argentina
x 1378

Re: Building tangent vectors

Post by dark_sylinc »

importV1() was designed to be called on an empty Mesh. It appears you're calling it on an already populated one. This may lead to strange behaviors. For example SubMesh::importBuffersFromV1 will append to mVao[] but the existing vaos are not removed.

It may be easier to just destroy the original v2 mesh and create a new one where the v1 can be imported into.
Or maybe calling inMesh->unload() before importing v1 -> v2 is enough.

jwwalker
Goblin
Posts: 291
Joined: Thu Aug 12, 2021 10:06 pm
Location: San Diego, CA, USA
x 21

Re: Building tangent vectors

Post by jwwalker »

Thanks! It does seem to work if I create a new mesh. For the record:

Code: Select all

static Ogre::MeshPtr AddTangentVectors( Ogre::MeshPtr inMesh, TQ3Object _Nonnull inObject )
{
	// Inconveniently, we must convert to a temporary v1 mesh in order
	// to create tangents.
	Ogre::v1::MeshPtr m1 = Ogre::v1::MeshManager::getSingleton().create(
		"TempV1ForTangents", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME );
	m1->setVertexBufferPolicy( Ogre::v1::HardwareBuffer::HBU_STATIC, false );
	m1->setIndexBufferPolicy( Ogre::v1::HardwareBuffer::HBU_DYNAMIC, false );
	m1->importV2( inMesh.get() );
	m1->buildTangentVectors();
	
	// Make a fresh Mesh
	std::string meshName( inMesh->getName() + "+T" );
	Ogre::MeshPtr m2 = Ogre::MeshManager::getSingleton().createManual( meshName,
		Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME );
	m2->importV1( m1.get(), false, false, false );
	m2->_setBounds( inMesh->getAabb() );
	Ogre::v1::MeshManager::getSingleton().remove( "TempV1ForTangents" );
	
	return m2;
}

With this, I got a normal map to work, to some degree. I'm working with a test model from the glTF file format. I'm wondering whether the tangent generation may not be doing what the glTF folks expect when the texture coordinates wrap. They expect the model to look something like this (with the ball on the right having a normal map):

Image

Whereas I got this:

Image

User avatar
dark_sylinc
OGRE Team Member
OGRE Team Member
Posts: 5509
Joined: Sat Jul 21, 2007 4:55 pm
Location: Buenos Aires, Argentina
x 1378

Re: Building tangent vectors

Post by dark_sylinc »

There are two possible problems:

  1. Mikkt space (Github) is the superior tangent generation method, and we haven't implemented it.
  2. It's possible the normal map is incorrectly loaded. If loaded as R8G8_SNORM (or BC5_SNORM, its compressed version), the values should be properly in range [-1; 1]. If messed up, the values might be in range [0; -1] U [0; 1], that is R8G8_UNORM got reinterpreted into R8G8_SNORM instead of being converted. Another possibility is that sRGB got in the way during conversion, causing normals to get bent and messed up. There's no true way to detect this, but there are a couple tells by looking at renderdoc: in the png RGB 0, 0, 128 should look as 0, 0 in R8G8_SNORM. And RGB 217, 217, 0 should look as 0.707 0.707 0 in R8G8_SNORM and 37 37 0 as -0.707 -0.707 0. Basically take the same pixel in Photoshop and do (pixel / 255) * 2 - 1.0 and it should look the same in R8G8_SNORM.
jwwalker
Goblin
Posts: 291
Joined: Thu Aug 12, 2021 10:06 pm
Location: San Diego, CA, USA
x 21

Re: Building tangent vectors

Post by jwwalker »

Great! Your second guess was spot on. I'm new to normal mapping, and didn't know that PFG_RG8_SNORM was the expected format. By the way, I found that the lazy way to ensure the right pixel format is to pass the flag TextureFilter::TypePrepareForNormalMapping when creating the texture.