//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "lighting/basic/blTerrainSystem.h" #include "core/bitVector.h" #include "lighting/common/shadowVolumeBSP.h" #include "lighting/lightingInterfaces.h" #include "terrain/terrData.h" #include "lighting/basic/basicLightManager.h" #include "lighting/common/sceneLighting.h" #include "gfx/bitmap/gBitmap.h" #include "collision/collision.h" extern SceneLighting* gLighting; struct blTerrainChunk : public PersistInfo::PersistChunk { typedef PersistInfo::PersistChunk Parent; blTerrainChunk(); ~blTerrainChunk(); GBitmap *mLightmap; bool read(Stream &); bool write(Stream &); }; //------------------------------------------------------------------------------ // Class SceneLighting::TerrainChunk //------------------------------------------------------------------------------ blTerrainChunk::blTerrainChunk() { mChunkType = PersistChunk::TerrainChunkType; mLightmap = NULL; } blTerrainChunk::~blTerrainChunk() { if(mLightmap) delete mLightmap; } //------------------------------------------------------------------------------ bool blTerrainChunk::read(Stream & stream) { if(!Parent::read(stream)) return(false); mLightmap = new GBitmap(); return mLightmap->readBitmap("png",stream); } bool blTerrainChunk::write(Stream & stream) { if(!Parent::write(stream)) return(false); if(!mLightmap) return(false); if(!mLightmap->writeBitmap("png",stream)) return(false); return(true); } class blTerrainProxy : public SceneLighting::ObjectProxy { protected: typedef ObjectProxy Parent; BitVector mShadowMask; ShadowVolumeBSP * mShadowVolume; LinearColorF * mLightmap; /// The dimension of the lightmap in pixels. const U32 mLightMapSize; /// The dimension of the terrain height map sample array. const U32 mTerrainBlockSize; LinearColorF *sgBakedLightmap; Vector sgLights; bool sgMarkStaticShadow(void *terrainproxy, SceneObject *sceneobject, LightInfo *light); //void postLight(bool lastLight); void lightVector(LightInfo *); struct SquareStackNode { U8 mLevel; U16 mClipFlags; Point2I mPos; }; S32 testSquare(const Point3F &, const Point3F &, S32, F32, const Vector &); bool markObjectShadow(ObjectProxy *); bool sgIsCorrectStaticObjectType(SceneObject *obj); inline LinearColorF _getValue( S32 row, S32 column ); public: blTerrainProxy(SceneObject * obj); ~blTerrainProxy(); TerrainBlock * operator->() {return(static_cast(static_cast(mObj)));} TerrainBlock * getObject() {return(static_cast(static_cast(mObj)));} bool getShadowedSquares(const Vector &, Vector &); // lighting void init(); bool preLight(LightInfo *); void light(LightInfo *); // persist U32 getResourceCRC(); bool setPersistInfo(PersistInfo::PersistChunk *); bool getPersistInfo(PersistInfo::PersistChunk *); virtual bool supportsShadowVolume(); virtual void getClipPlanes(Vector& planes); virtual void addToShadowVolume(ShadowVolumeBSP * shadowVolume, LightInfo * light, S32 level); // events //virtual void processTGELightProcessEvent(U32 curr, U32 max, LightInfo* currlight); //virtual void processSGObjectProcessEvent(LightInfo* currLight); }; //------------------------------------------------------------------------------- // Class SceneLighting::TerrainProxy: //------------------------------------------------------------------------------- blTerrainProxy::blTerrainProxy( SceneObject *obj ) : Parent( obj ), mLightMapSize( getObject()->getLightMapSize() ), mShadowVolume( NULL ), mTerrainBlockSize( getObject()->getBlockSize() ), mLightmap( NULL ), sgBakedLightmap( NULL ) { } blTerrainProxy::~blTerrainProxy() { delete [] mLightmap; } //------------------------------------------------------------------------------- void blTerrainProxy::init() { mLightmap = new LinearColorF[ mLightMapSize * mLightMapSize ]; dMemset(mLightmap, 0, mLightMapSize * mLightMapSize * sizeof(LinearColorF)); mShadowMask.setSize( mTerrainBlockSize * mTerrainBlockSize ); } bool blTerrainProxy::preLight(LightInfo * light) { if(!bool(mObj)) return(false); if(light->getType() != LightInfo::Vector) return(false); mShadowMask.clear(); return(true); } inline LinearColorF blTerrainProxy::_getValue( S32 row, S32 column ) { while( row < 0 ) row += mLightMapSize; row = row % mLightMapSize; while( column < 0 ) column += mLightMapSize; column = column % mLightMapSize; U32 offset = row * mLightMapSize + column; return mLightmap[offset]; } bool blTerrainProxy::markObjectShadow(ObjectProxy * proxy) { if (!proxy->supportsShadowVolume()) return false; // setup the clip planes Vector clipPlanes; proxy->getClipPlanes(clipPlanes); Vector shadowList; if(!getShadowedSquares(clipPlanes, shadowList)) return(false); // set the correct bit for(U32 i = 0; i < shadowList.size(); i++) mShadowMask.set(shadowList[i]); return(true); } void blTerrainProxy::light(LightInfo * light) { // If we don't have terrain or its not a directional // light then skip processing. TerrainBlock * terrain = getObject(); if ( !terrain || light->getType() != LightInfo::Vector ) return; S32 time = Platform::getRealMilliseconds(); // reset mShadowVolume = new ShadowVolumeBSP; // build interior shadow volume for(ObjectProxy ** itr = gLighting->mLitObjects.begin(); itr != gLighting->mLitObjects.end(); itr++) { ObjectProxy* objproxy = *itr; if (markObjectShadow(objproxy)) objproxy->addToShadowVolume(mShadowVolume, light, SceneLighting::SHADOW_DETAIL); } lightVector(light); // set the lightmap... terrain->clearLightMap(); // Blur... F32 kernel[3][3] = { {1, 2, 1}, {2, 3, 2}, {1, 2, 1} }; F32 modifier = 1; F32 divisor = 0; for( U32 i=0; i<3; i++ ) { for( U32 j=0; j<3; j++ ) { if( i==1 && j==1 ) { kernel[i][j] = 1 + kernel[i][j] * modifier; } else { kernel[i][j] = kernel[i][j] * modifier; } divisor += kernel[i][j]; } } for( U32 i=0; i < mLightMapSize; i++ ) { for( U32 j=0; j < mLightMapSize; j++ ) { LinearColorF val; val = _getValue( i-1, j-1 ) * kernel[0][0]; val += _getValue( i-1, j ) * kernel[0][1]; val += _getValue( i-1, j+1 ) * kernel[0][2]; val += _getValue( i, j-1 ) * kernel[1][0]; val += _getValue( i, j ) * kernel[1][1]; val += _getValue( i, j+1 ) * kernel[1][2]; val += _getValue( i+1, j-1 ) * kernel[2][0]; val += _getValue( i+1, j ) * kernel[2][1]; val += _getValue( i+1, j+1 ) * kernel[2][2]; U32 edge = 0; if( j == 0 || j == mLightMapSize - 1 ) edge++; if( i == 0 || i == mLightMapSize - 1 ) edge++; if( !edge ) val = val / divisor; else val = mLightmap[ i * mLightMapSize + j ]; // clamp values mLightmap[ i * mLightMapSize + j ]= val; } } // And stuff it into the texture... GBitmap *terrLightMap = terrain->getLightMap(); for(U32 y = 0; y < mLightMapSize; y++) { for(U32 x = 0; x < mLightMapSize; x++) { ColorI color(255, 255, 255, 255); color.red = mLightmap[x + y * mLightMapSize].red * 255; color.green = mLightmap[x + y * mLightMapSize].green * 255; color.blue = mLightmap[x + y * mLightMapSize].blue * 255; terrLightMap->setColor(x, y, color); } } /* // This handles matching up the outer edges of the terrain // lightmap when it has neighbors if (!terrain->isTiling()) { for (S32 y = 0; y < terrLightMap->getHeight(); y++) { ColorI c; if (terrain->getFile()->mEdgeTerrainFiles[0]) { terrLightMap->getColor(terrLightMap->getWidth()-1,y,c); terrLightMap->setColor(0,y,c); terrLightMap->setColor(1,y,c); } else { terrLightMap->getColor(0,y,c); terrLightMap->setColor(terrLightMap->getWidth()-1,y,c); terrLightMap->setColor(terrLightMap->getWidth()-2,y,c); } } for (S32 x = 0; x < terrLightMap->getHeight(); x++) { ColorI c; if (terrain->getFile()->mEdgeTerrainFiles[1]) { terrLightMap->getColor(x,terrLightMap->getHeight()-1,c); terrLightMap->setColor(x,0,c); terrLightMap->setColor(x,1,c); } else { terrLightMap->getColor(x,0,c); terrLightMap->setColor(x,terrLightMap->getHeight()-1,c); terrLightMap->setColor(x,terrLightMap->getHeight()-2,c); } } } */ delete mShadowVolume; Con::printf(" = terrain lit in %3.3f seconds", (Platform::getRealMilliseconds()-time)/1000.f); } //------------------------------------------------------------------------------ S32 blTerrainProxy::testSquare(const Point3F & min, const Point3F & max, S32 mask, F32 expand, const Vector & clipPlanes) { expand = 0; S32 retMask = 0; Point3F minPoint, maxPoint; for(S32 i = 0; i < clipPlanes.size(); i++) { if(mask & (1 << i)) { if(clipPlanes[i].x > 0) { maxPoint.x = max.x; minPoint.x = min.x; } else { maxPoint.x = min.x; minPoint.x = max.x; } if(clipPlanes[i].y > 0) { maxPoint.y = max.y; minPoint.y = min.y; } else { maxPoint.y = min.y; minPoint.y = max.y; } if(clipPlanes[i].z > 0) { maxPoint.z = max.z; minPoint.z = min.z; } else { maxPoint.z = min.z; minPoint.z = max.z; } F32 maxDot = mDot(maxPoint, clipPlanes[i]); F32 minDot = mDot(minPoint, clipPlanes[i]); F32 planeD = clipPlanes[i].d; if(maxDot <= -(planeD + expand)) return(U16(-1)); if(minDot <= -planeD) retMask |= (1 << i); } } return(retMask); } bool blTerrainProxy::getShadowedSquares(const Vector & clipPlanes, Vector & shadowList) { TerrainBlock *terrain = getObject(); if ( !terrain ) return false; // TODO: Fix me for variable terrain sizes! return true; /* SquareStackNode stack[TerrainBlock::BlockShift * 4]; stack[0].mLevel = TerrainBlock::BlockShift; stack[0].mClipFlags = 0xff; stack[0].mPos.set(0,0); U32 stackSize = 1; Point3F blockPos; terrain->getTransform().getColumn(3, &blockPos); S32 squareSize = terrain->getSquareSize(); F32 floatSquareSize = (F32)squareSize; bool marked = false; // push through all the levels of the quadtree while(stackSize) { SquareStackNode * node = &stack[stackSize - 1]; S32 clipFlags = node->mClipFlags; Point2I pos = node->mPos; GridSquare * sq = terrain->findSquare(node->mLevel, pos); Point3F minPoint, maxPoint; minPoint.set(squareSize * pos.x + blockPos.x, squareSize * pos.y + blockPos.y, fixedToFloat(sq->minHeight)); maxPoint.set(minPoint.x + (squareSize << node->mLevel), minPoint.y + (squareSize << node->mLevel), fixedToFloat(sq->maxHeight)); // test the square against the current level if(clipFlags) { clipFlags = testSquare(minPoint, maxPoint, clipFlags, floatSquareSize, clipPlanes); if(clipFlags == U16(-1)) { stackSize--; continue; } } // shadowed? if(node->mLevel == 0) { marked = true; shadowList.push_back(pos.x + (pos.y << TerrainBlock::BlockShift)); stackSize--; continue; } // setup the next level of squares U8 nextLevel = node->mLevel - 1; S32 squareHalfSize = 1 << nextLevel; for(U32 i = 0; i < 4; i++) { node[i].mLevel = nextLevel; node[i].mClipFlags = clipFlags; } node[3].mPos = pos; node[2].mPos.set(pos.x + squareHalfSize, pos.y); node[1].mPos.set(pos.x, pos.y + squareHalfSize); node[0].mPos.set(pos.x + squareHalfSize, pos.y + squareHalfSize); stackSize += 3; } return marked; */ } void blTerrainProxy::lightVector(LightInfo * light) { // Grab our terrain object TerrainBlock* terrain = getObject(); if (!terrain) return; // Get the direction to the light (the inverse of the direction // the light is pointing) Point3F lightDir = -light->getDirection(); lightDir.normalize(); // Get the ratio between the light map pixel and world space (used below) F32 lmTerrRatio = (F32)mTerrainBlockSize / (F32) mLightMapSize; lmTerrRatio *= terrain->getSquareSize(); U32 i = 0; for (U32 y = 0; y < mLightMapSize; y++) { for (U32 x = 0; x < mLightMapSize; x++) { // Get the relative pixel position and scale it // by the ratio between lightmap and world space Point2F pixelPos(x, y); pixelPos *= lmTerrRatio; // Start with a default normal of straight up Point3F normal(0.0f, 0.0f, 1.0f); // Try to get the actual normal from the terrain. // Note: this won't change the default normal if // it can't find a normal. terrain->getNormal(pixelPos, &normal); // The terrain lightmap only contains shadows. F32 shadowed = 0.0f; // Get the height at the lightmap pixel's position F32 height = 0.0f; terrain->getHeight(pixelPos, &height); // Calculate the 3D position of the pixel Point3F pixelPos3F(pixelPos.x, pixelPos.y, height); // Translate that position by the terrain's transform terrain->getTransform().mulP(pixelPos3F); // Offset slighting along the normal so that we don't // raycast into ourself pixelPos3F += (normal * 0.1f); // Calculate the light's position. // If it is a vector light like the sun (no position // just direction) then translate along that direction // a reasonable distance to get a point sufficiently // far away Point3F lightPos = light->getPosition(); if(light->getType() == LightInfo::Vector) { lightPos = 1000.f * lightDir; lightPos = pixelPos3F + lightPos; } // Cast a ray from the world space position of the lightmap pixel to the light source. // If we hit something then we are in shadow. This allows us to be shadowed by anything // that supports a castRay operation. RayInfo info; if(terrain->getContainer()->castRay(pixelPos3F, lightPos, STATIC_COLLISION_TYPEMASK, &info)) { // Shadow the pixel. shadowed = 1.0f; } // Set the final lightmap color. mLightmap[i++] += LinearColorF::WHITE * mClampF( 1.0f - shadowed, 0.0f, 1.0f ); } } } //-------------------------------------------------------------------------- U32 blTerrainProxy::getResourceCRC() { TerrainBlock * terrain = getObject(); if(!terrain) return(0); return(terrain->getCRC()); } //-------------------------------------------------------------------------- bool blTerrainProxy::setPersistInfo(PersistInfo::PersistChunk * info) { if(!Parent::setPersistInfo(info)) return(false); blTerrainChunk * chunk = dynamic_cast(info); AssertFatal(chunk, "blTerrainProxy::setPersistInfo: invalid info chunk!"); TerrainBlock * terrain = getObject(); if(!terrain || !terrain->getLightMap()) return(false); terrain->setLightMap( new GBitmap( *chunk->mLightmap) ); return(true); } bool blTerrainProxy::getPersistInfo(PersistInfo::PersistChunk * info) { if(!Parent::getPersistInfo(info)) return(false); blTerrainChunk * chunk = dynamic_cast(info); AssertFatal(chunk, "blTerrainProxy::getPersistInfo: invalid info chunk!"); TerrainBlock * terrain = getObject(); if(!terrain || !terrain->getLightMap()) return(false); if(chunk->mLightmap) delete chunk->mLightmap; chunk->mLightmap = new GBitmap(*terrain->getLightMap()); return(true); } bool blTerrainProxy::supportsShadowVolume() { return false; } void blTerrainProxy::getClipPlanes(Vector& planes) { } void blTerrainProxy::addToShadowVolume(ShadowVolumeBSP * shadowVolume, LightInfo * light, S32 level) { } void blTerrainSystem::init() { } U32 blTerrainSystem::addObjectType() { return TerrainObjectType; } SceneLighting::ObjectProxy* blTerrainSystem::createObjectProxy(SceneObject* obj, SceneLighting::ObjectProxyList* sceneObjects) { if ((obj->getTypeMask() & TerrainObjectType) != 0) return new blTerrainProxy(obj); else return NULL; } PersistInfo::PersistChunk* blTerrainSystem::createPersistChunk(const U32 chunkType) { if (chunkType == PersistInfo::PersistChunk::TerrainChunkType) return new blTerrainChunk(); else return NULL; } bool blTerrainSystem::createPersistChunkFromProxy(SceneLighting::ObjectProxy* objproxy, PersistInfo::PersistChunk **ret) { if (dynamic_cast(objproxy) != NULL) { *ret = new blTerrainChunk(); return true; } return false; } // Given a ray, this will return the color from the lightmap of this object, return true if handled bool blTerrainSystem::getColorFromRayInfo(const RayInfo & collision, LinearColorF& result) const { TerrainBlock *terrain = dynamic_cast(collision.object); if (!terrain) return false; Point2F uv; F32 terrainlength = (F32)terrain->getBlockSize(); Point3F pos = terrain->getPosition(); uv.x = (collision.point.x - pos.x) / terrainlength; uv.y = (collision.point.y - pos.y) / terrainlength; // similar to x = x & width... uv.x = uv.x - F32(U32(uv.x)); uv.y = uv.y - F32(U32(uv.y)); const GBitmap* lightmap = terrain->getLightMap(); if (!lightmap) return false; result = lightmap->sampleTexel(uv.x, uv.y); // terrain lighting is dim - look into this (same thing done in shaders)... result *= 2.0f; return true; }