Jump to content

Building Cubemaps In Leadwerks.

reepblue

2,107 views

blog-0233614001460007417.jpg

With the recent popular substance shader by Shadmar, more talk and focus has been about cubemap reflections surfaced in the community. Reflections in Leadwerks were done in the past with the SSLR post process shader which made all surfaces reflect the world in real time.

 

 

 

 

However, it had the following problems:

  • It was applied to ALL materials in the scene. If you had a material that wouldn't normally reflect such as concrete or wood, the shader would still effect those materials. Basically, the texture artist had little control.
  • The post process shader needed to be first on the list, and some people didn't understand that.
  • You could adjust the reflections by the model's alpha color. But if you had any shader that relied on the alpha mask for map blending or something else, you're out of luck!
  • The reflections distorted based on camera angle, and not really great for in-game action.
  • Like everything OpenGL, there is/was a chance it wouldn't work well with AMD gpus.

 

This was all we had until in February, Mattline1 posted a blog on his PBR system, which included a block of code that built a world cubemap and swapped the cubemap texture with it. Thankfully, he uploaded is source code so anyone who has the professional edition could download his code and have a look.

 

While his implementation is very nice, it has the following design flaws for a typical Leadwerks user.

  • You needed to use only the PBR shader. Normal Leadwerks shaders didn't render well.
  • Needed a Post process effect to adjust the gamma of the scene.

 

Weeks later, Shadmar uploaded his version of a Substance/PBR shader which allowed users to still use their existing shaders, and gave artists more to work. Since it's a shader, and not a full implementation like Matt's, it didn't have the world reflection system. I've been busy with a lot of Source engine work, but seeing how much interest this is getting lately, I decided to take a few days off and see if I can make my own cubemap system.

 

 

The Implementation

 

A disclaimer, this is not a full "How-To" as you'll need to add it to how your application, and it'll most likely do things differently than how I have my application set up.

 

First, we need to figure out how we are gonna get a world cubemap to a texture. While it's not the most "next-gen" way of doing this, I decided to base my system off of the Source engine because, well I know how it works, and it's pretty simple. It goes like this:

  1. Mapper places a env_cubemap entity near a shinny surface. (Any material set to use cubemaps.)
     
  2. Mapper compiles and runs the engine.
     
  3. Mapper than types "buildcubemaps" into the console.
     
  4. For each env_cubemap entity, 6 "pictures" are taken (up, down, left, right, forward, and back). than those textures are saved within the bsp.
     
  5. The map restarts, and the surface will use the near by cubemap texture.

 

Great, that seems very straight forward, and we can remove steps 2 and 3 as there is no compling in Leadwerks, and everything should just work. The first thing we need is a 3D point in the world that'll act as our env_cubemap.

 


class Cubemap : public Pivot

{

public:

Cubemap(Vec3 pos = NULL); //lua

virtual ~Cubemap() {}

...

...

...

};

 

We build off of the pivot class because we want this to actually exist in our world, not be a "puppeteer" for a pivot entity. We also have the position being set in the constructor as the only way to get our custom entity into the world is by a lua script. Using ToLua++ we can make a script that we can attach to a pivot/sprite.

 

function Script:Start()
local n = Cubemap:new(self.entity:GetPosition(true))
self.entity:Hide()
end

 

This script will spawn our cubemap entity at our editor placed pivot, then hide the editor placed pivot. Basically, we swap the entities during level load.

 

This cubemap entity job is to search for near by entites, (Which I currently have set to it's aabb radius + a range float) then change it's surface material with a copy of what it had, only with a different cubemap texture. Early in development, I just have the entity fix the cubemap texture to the current world's skybox to make sure that this would work.

 


 

 

void Cubemap::SetReflectionTextureToSkybox(Leadwerks::Surface* surf)

{

if (WORLD == NULL) { return; }

if (WORLD->skyboxpath == "") { return; }

if (!surf) { return; }

 

Material* mat = surf->GetMaterial();

if (!mat) { return; }

 

// Make this faster, if there is any indication of a surface not using the substance shader, abort the cubemap application!

 

// If there is no cubemap assigned, then we can skip this object.

if (mat->GetTexture(6) == nullptr) return;

 

// if the metal value is 0, we can skip.

int matspecmetalness = mat->GetColor(COLOR_SPECULAR).w;

if (matspecmetalness == 0 ) return;

 

Material* copy = (Material*)mat->Copy();

 

if (copy != NULL)

{

Texture* cubeTexture = Texture::Load(WORLD->skyboxpath);

copy->SetTexture(cubeTexture, 6);

surf->SetMaterial(copy);

 

cubeTexture->Release();

copy->Release();

}

}

 

Before we go into building the world cubemaps, we need to add this to the entity's header. This will make it so we can pass a texture to the entity:

 


Texture* SavedMap;

Texture* GetCubemap()

{

return SavedMap;

}

 

Now to the cubemap factory, which on map load will find all of our cubemap entites, do the texture building or each entity, Set SavedMap to the new cubemap texture, then tell the cubemap entity to look for near by entities so it can do it's replacement job. Thankfully, all the yucky stuff has been done by Matt, so I just needed to cut the stuff I didn't need, and make it handle multiple level loads and disconnects to work with my worldmanager class.

 

Here's a few snip of this process.

 


 

bool CubemapFactory::initialize()

{

Release();

...

...

...

 

int i = 0;

do

{

if (WORLD->GetEntity(i)->GetKeyValue("cubemap", "0") == "1")

{

Cubemap* pCubemap = dynamic_cast<Cubemap*>(WORLD->GetEntity(i));

if (pCubemap != NULL)

{

// Build the cubemap

cubeTexture = Leadwerks::Texture::CubeMap(

reflectionResolution,

reflectionResolution,

Leadwerks::Texture::RGBA,

Leadwerks::Texture::Mipmaps

);

 

cubeTexture->BuildMipmaps();

Leadwerks::OpenGL2Texture* gl2cubeTexture = dynamic_cast<Leadwerks::OpenGL2Texture*>(cubeTexture);

glActiveTexture(gl2cubeTexture->GetGLTarget());

glBindTexture(GL_TEXTURE_CUBE_MAP, gl2cubeTexture->gltexturehandle);

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);

glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

 

 

// Apply it!

GenerateReflection(pCubemap->GetPosition()); //<-- generate first reflection texture

GenerateReflection(pCubemap->GetPosition()); //<-- use initial texture to generate correct texture

if (cubeTexture != nullptr)

{

pCubemap->SavedMap = cubeTexture;

}

 

// Tell the cubemap to look for near by entities, and replace it's cubemap texture with cubeTexture

pCubemap->SearchForEntites();

cubeTexture->Release();

}

}

i++;

} while (i < WORLD->CountEntities());

...

...

...

CubemapFactory::init = true;

}

 

GenerateReflection() is pretty much the same as Matt's PBR project. That function actually does the "camera work", and saves the result in a Texture. This is done on the fly during level transition.

 

 

Skipping Dynamic Objects

 

There is just one problem however. Everything is being rendered when the cubemaps are being built. So any enemies, or move-able objects will be baked into a reflection. To fix this, we need to have all objects that can move (Mostly have mass) be hidden, and then re-shown after we've finished the building of cubemaps.

 

Pretty much.

 


#define S_NULL ""

#define ON "1"

 

void ShowMoveables(Entity* entity)

{

if (entity != NULL)

{

 

if (entity->Hidden() == true && entity->GetKeyValue("hide_for_cubemap", S_NULL) == ON)

{

entity->Show();

entity->AddForce(Vec3(0, 0, 0)); // <-Wake me!

entity->SetKeyValue("hide_for_cubemap", S_NULL);

}

}

}

 

void HideMoveables(Entity* entity)

{

if (entity != NULL)

{

// Things to look for ar entites with the mass > 0, or have their shadow mode non static!

if (entity->GetMass() == 0 || entity->GetShadowMode() == Light::Static)

return;

 

// Only hide the non-hidden!

if (entity->Hidden() == false)

{

entity->SetKeyValue("hide_for_cubemap", ON);

entity->Hide();

}

}

}

 

void MapHook(Entity* entity, Object* extra)

{

// For cubemap generation, we don't want anything that's set to move to be built in with our cubemaps.

HideMoveables(entity);

}

 

 

bool WorldManager::Connect(std::string pMapname)

{

...

 

if (Map::Load(pMapname, &MapHook) == false)

return false;

...

return true;

}

 

 

//--------------------------------------

// For cubemap building!

//--------------------------------------

void WorldManager::MakeMoveablesVisible()

{

vector<Entity*>::iterator iter = entities.begin();

for (iter; iter != entities.end(); iter++)

{

Entity* entity = *iter;

ShowMoveables(entity);

}

}

 

Then back in the initialize function of our cubemap factory before we return true.

 


 

// Lastly, redraw moveables!

worldmanager.MakeMoveablesVisible();

 

And now anything that has mass, or has it's ShadowMode set to Dynamic will not be baked into the reflection!

 

 

Cleaning up

 

To make it so that this can work with multiple level loads and when the game "disconnects" (Not in a map), we need to reset everything.

 

Cubemap factory:

 


 

void CubemapFactory::Render()

{

if (WORLD != NULL || worldmanager.IsConnected() == true)

{

if (!CubemapFactory::init) { CubemapFactory::initialize(); }

WORLD->Render();

}

}

 

void CubemapFactory::Release()

{

System::Print("Clearing Cubemaps!");

CubemapFactory::init = false;

if (cubeTexture != NULL)

{

cubeTexture->Release();

}

CubemapFactory::currentContext = nullptr;

CubemapFactory::reflectionCamera = nullptr;

CubemapFactory::cubeBuffer = nullptr;

CubemapFactory::cubeTexture = nullptr;

}

 

And instead of calling world->Clear in my world manager, I instead call this function:

 


 

void WorldManager::ClearWorld()

{

CubemapFactory::Release();

world->Clear();

}

 

Calling CubemapFactory::Release() may not be necessary as it's called everytime the factory is initialized, but it doesn't hurt to be safe!

 

 

Conclusion

 

It was a ride getting this to work correctly. I'm still not happy about how the cubemaps look for near by entities, but it's better than what we have now, which is nothing, Apperently, Josh is concidering this as a engine feature so all of this work might just be temp until he adds it in, which hopefully will be smoother and less error prone!

 

I may consider adding "environment probes" like Source has. They would basically work exactly the same way as what Scrotie did.

 

But what we have a system that works like this:

  1. Engine loads a map, world created, etc.
     
  2. A map hook hides any entities that has mass or a dynamic shadow mode.
     
  3. Cubemap Factory takes it's "pictures" much like Source does when buildcubemaps is executed.
     
  4. Each cubemap texture is given to the right Cubemap entity, then released.
     
  5. The cubemap looks for near by entites and swaps the model surface's default cubemap with the one it got from the factory.
     
  6. Cubemap factory tells the world manager to re-show the entities it hid.
  7. Things continue as normal.

 

 

 

So that's it! Thanks for reading!



4 Comments


Recommended Comments

Great stuff!

 

I've been testing using Matts' mipmap code aswell, but I added a small lua script to each reflected object, inorder to make a local cubemap (onetime render)

My main problem was getting the mipmapping correctly in lua.

Share this comment


Link to comment

With the recent beta update(s), this doesn't work anymore! D: Hopefully official probe support comes soon.

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...