/* Copyright (c) 2013 yvt This file is part of OpenSpades. OpenSpades is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OpenSpades is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenSpades. If not, see . */ #include "GLSparseShadowMapRenderer.h" #include "IGLDevice.h" #include "GLRenderer.h" #include "../Core/Debug.h" #include "../Core/Exception.h" #include "../Core/Settings.h" #include "GLProfiler.h" #include "GLModelRenderer.h" #include "GLRenderer.h" #include "GLModel.h" SPADES_SETTING(r_shadowMapSize, "2048"); namespace spades { namespace draw { GLSparseShadowMapRenderer::GLSparseShadowMapRenderer(GLRenderer *r): IGLShadowMapRenderer(r){ SPADES_MARK_FUNCTION(); device = r->GetGLDevice(); if((int)r_shadowMapSize > 4096) { SPLog("r_shadowMapSize is too large; changed to 4096"); r_shadowMapSize = 4096; } textureSize = r_shadowMapSize; for(minLod = 0; (Tiles << minLod) < textureSize; minLod++); //minLod = std::max(0, minLod - 2); minLod = 0; maxLod = 15; colorTexture = device->GenTexture(); device->BindTexture(IGLDevice::Texture2D, colorTexture); device->TexImage2D(IGLDevice::Texture2D, 0, IGLDevice::RGB, textureSize, textureSize, 0, IGLDevice::RGB, IGLDevice::UnsignedByte, NULL); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMagFilter, IGLDevice::Linear); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMinFilter, IGLDevice::Linear); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureWrapS, IGLDevice::ClampToEdge); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureWrapT, IGLDevice::ClampToEdge); texture = device->GenTexture(); device->BindTexture(IGLDevice::Texture2D, texture); device->TexImage2D(IGLDevice::Texture2D, 0, IGLDevice::DepthComponent24, textureSize, textureSize, 0, IGLDevice::DepthComponent, IGLDevice::UnsignedInt, NULL); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMagFilter, IGLDevice::Linear); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMinFilter, IGLDevice::Linear); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureWrapS, IGLDevice::ClampToEdge); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureWrapT, IGLDevice::ClampToEdge); pagetableTexture = device->GenTexture(); device->BindTexture(IGLDevice::Texture2D, pagetableTexture); device->TexImage2D(IGLDevice::Texture2D, 0, IGLDevice::RGBA8, Tiles, Tiles, 0, IGLDevice::BGRA, IGLDevice::UnsignedByte, NULL); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMagFilter, IGLDevice::Nearest); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMinFilter, IGLDevice::Nearest); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureWrapS, IGLDevice::ClampToEdge); device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureWrapT, IGLDevice::ClampToEdge); framebuffer = device->GenFramebuffer(); device->BindFramebuffer(IGLDevice::Framebuffer, framebuffer); device->FramebufferTexture2D(IGLDevice::Framebuffer, IGLDevice::ColorAttachment0, IGLDevice::Texture2D, colorTexture, 0); device->FramebufferTexture2D(IGLDevice::Framebuffer, IGLDevice::DepthAttachment, IGLDevice::Texture2D, texture, 0); device->BindFramebuffer(IGLDevice::Framebuffer, 0); } GLSparseShadowMapRenderer::~GLSparseShadowMapRenderer(){ SPADES_MARK_FUNCTION(); device->DeleteTexture(texture); device->DeleteTexture(pagetableTexture); device->DeleteFramebuffer(framebuffer); device->DeleteTexture(colorTexture); } #define Segment GLSSMRSegment struct Segment { float low, high; bool empty; Segment():empty(true){} Segment(float l, float h): low(std::min(l,h)), high(std::max(l,h)),empty(false){} void operator +=(const Segment& seg) { if(seg.empty) return; if(empty){ low = seg.low; high = seg.high; empty = false; }else{ low = std::min(low, seg.low); high = std::max(high, seg.high); } } void operator +=(float v){ if(empty){ low = high = v; }else{ if(v < low)low = v; if(v > high) high = v; } } }; static Vector3 FrustrumCoord(const client::SceneDefinition& def, float x, float y, float z){ x *= z; y *= z; return def.viewOrigin + def.viewAxis[0] * x + def.viewAxis[1] * y + def.viewAxis[2] * z; } static Segment ZRange(Vector3 base, Vector3 dir, Plane3 plane1, Plane3 plane2){ return Segment(plane1.GetDistanceTo(base)/Vector3::Dot(dir,plane1.n), plane2.GetDistanceTo(base)/Vector3::Dot(dir,plane2.n)); } void GLSparseShadowMapRenderer::BuildMatrix(float near, float far){ // TODO: variable light direction? Vector3 lightDir = MakeVector3(0, -1, -1).Normalize(); // set better up dir? Vector3 up = MakeVector3(0, 0, 1); Vector3 side = Vector3::Cross(up, lightDir).Normalize(); up = Vector3::Cross(lightDir, side).Normalize(); // build frustrum client::SceneDefinition def = GetRenderer()->GetSceneDef(); Vector3 frustrum[8]; float tanX = tanf(def.fovX * .5f); float tanY = tanf(def.fovY * .5f); frustrum[0] = FrustrumCoord(def, tanX, tanY, near); frustrum[1] = FrustrumCoord(def, tanX, -tanY, near); frustrum[2] = FrustrumCoord(def, -tanX, tanY, near); frustrum[3] = FrustrumCoord(def, -tanX, -tanY, near); frustrum[4] = FrustrumCoord(def, tanX, tanY, far); frustrum[5] = FrustrumCoord(def, tanX, -tanY, far); frustrum[6] = FrustrumCoord(def, -tanX, tanY, far); frustrum[7] = FrustrumCoord(def, -tanX, -tanY, far); // compute frustrum's x,y boundary float minX, maxX, minY, maxY; minX = maxX = Vector3::Dot(frustrum[0], side); minY = maxY = Vector3::Dot(frustrum[0], up); for(int i = 1; i < 8; i++){ float x = Vector3::Dot(frustrum[i], side); float y = Vector3::Dot(frustrum[i], up); if(x < minX) minX = x; if(x > maxX) maxX = x; if(y < minY) minY = y; if(y > maxY) maxY = y; } // compute frustrum's z boundary Segment seg; Plane3 plane1(0,0,1,-4.f); Plane3 plane2(0,0,1,64.f); seg += ZRange(side * minX + up * minY, lightDir, plane1, plane2); seg += ZRange(side * minX + up * maxY, lightDir, plane1, plane2); seg += ZRange(side * maxX + up * minY, lightDir, plane1, plane2); seg += ZRange(side * maxX + up * maxY, lightDir, plane1, plane2); for(int i = 1; i < 8; i++){ seg += Vector3::Dot(frustrum[i], lightDir); } // build frustrum obb Vector3 origin = side * minX + up * minY + lightDir * seg.low; Vector3 axis1 = side * (maxX - minX); Vector3 axis2 = up * (maxY - minY); Vector3 axis3 = lightDir * (seg.high - seg.low); obb = OBB3(Matrix4::FromAxis(axis1, axis2, axis3, origin)); vpWidth = 2.f / axis1.GetLength(); vpHeight = 2.f / axis2.GetLength(); // convert to projectionview matrix matrix = obb.m.InversedFast(); matrix = Matrix4::Scale(2.f) * matrix; matrix = Matrix4::Translate(-1, -1, -1) * matrix; // scale a little big for padding matrix = Matrix4::Scale(.98f) * matrix; // matrix = Matrix4::Scale(1,1,-1) * matrix; // make sure frustrums in range #ifndef NDEBUG for(int i = 0; i < 8; i++){ Vector4 v = matrix * frustrum[i]; SPAssert(v.x >= -1.f); SPAssert(v.y >= -1.f); //SPAssert(v.z >= -1.f); SPAssert(v.x < 1.f); SPAssert(v.y < 1.f); //SPAssert(v.z < 1.f); } #endif } void GLSparseShadowMapRenderer::Render() { SPADES_MARK_FUNCTION(); IGLDevice::Integer lastFb = device->GetInteger(IGLDevice::FramebufferBinding); //client::SceneDefinition def = GetRenderer()->GetSceneDef(); float nearDist = 0.f; float farDist = 150.f; BuildMatrix(nearDist, farDist); device->BindFramebuffer(IGLDevice::Framebuffer, framebuffer); device->Viewport(0, 0, textureSize, textureSize); device->ClearDepth(1.f); device->Clear(IGLDevice::DepthBufferBit); RenderShadowMapPass(); device->BindFramebuffer(IGLDevice::Framebuffer, lastFb); device->Viewport(0, 0, device->ScreenWidth(), device->ScreenHeight()); } #pragma mark - Sparse Processor static const size_t NoGroup = (size_t)-1; static const size_t NoInstance = (size_t)-1; static const size_t NoNode = (size_t)-1; static const size_t GroupNodeFlag = NoNode ^ (NoNode>>1); struct GLSparseShadowMapRenderer::Internal { GLSparseShadowMapRenderer *renderer; Vector3 cameraShadowCoord; typedef int LodUnit; struct Instance { GLModel *model; const client::ModelRenderParam *param; IntVector3 tile1, tile2; // int aabb2 size_t prev, next; // instance in the same group }; /** All Instances. Sorted by model somehow. */ std::vector allInstances; struct Group { IntVector3 tile1, tile2; // int aabb2 size_t firstInstance, lastInstance; LodUnit lod; bool valid; // shadow map texture coordinate int rawX, rawY, rawW, rawH; }; std::vector groups; // packing node struct Node { size_t child1, child2; int pos; bool horizontal; // true: x=pos, false: y=pos }; std::vector nodes; int lodBias; int mapSize; size_t groupMap[Tiles][Tiles]; static IntVector3 ShadowMapToTileCoord(Vector3 vec) { vec.x = (vec.x * (float)(Tiles / 2)) + (float)(Tiles / 2); vec.y = (vec.y * (float)(Tiles / 2)) + (float)(Tiles / 2); IntVector3 v; v.x = (int)floorf(vec.x); v.y = (int)floorf(vec.y);; return v; } static Vector3 TileToShadowMapCoord(IntVector3 tile) { Vector3 v; v.x = (float)tile.x * (2.f / (float)Tiles) - 1.f; v.y = (float)tile.y * (2.f / (float)Tiles) - 1.f; return v; } LodUnit ComputeLod(Vector3 shadowCoord) { float dx = shadowCoord.x - cameraShadowCoord.x; float dy = shadowCoord.y - cameraShadowCoord.y; float sq = dx * dx + dy * dy; float lod = 1.f / (sqrtf(sq) + .01f); int iv = (int)(lod * 200.f); if(iv < 1) iv = 1; if(iv > 65536) iv = 65535; int ld = 0; while(iv > 0){ ld++; iv>>=1; } return ld; } void FillGroupMap(IntVector3 tile1, IntVector3 tile2, size_t val){ for(int x = tile1.x; x < tile2.x; x++) for(int y = tile1.y; y < tile2.y; y++) groupMap[x][y] = val; } /** Combines `dest` and `src`. Group `src` will be * no longer valid. */ void CombineGroup(size_t dest, size_t src) { Group& g1 = groups[dest]; Group& g2 = groups[src]; FillGroupMap(g2.tile1, g2.tile2, dest); // extend the area g1.tile1.x = std::min(g1.tile1.x, g2.tile1.x); g1.tile1.y = std::min(g1.tile1.y, g2.tile1.y); g1.tile2.x = std::max(g1.tile2.x, g2.tile2.x); g1.tile2.y = std::max(g1.tile2.y, g2.tile2.y); g1.lod = std::max(g1.lod, g2.lod); // combine the instance list // [g1] + [g2] -> [g1, g2] Instance& destLast = allInstances[g1.lastInstance]; Instance& srcFirst = allInstances[g2.firstInstance]; SPAssert(destLast.next == NoInstance); SPAssert(srcFirst.prev == NoInstance); destLast.next = g2.firstInstance; srcFirst.prev = g1.lastInstance; g1.lastInstance = g2.lastInstance; // make sure the area is filled with the group `dest` IntVector3 tile1 = g1.tile1, tile2 = g1.tile2; for(int x = tile1.x; x < tile2.x; x++) for(int y = tile1.y; y < tile2.y; y++){ size_t g = groupMap[x][y]; SPAssert(g != src); if(g != NoGroup && g != dest) { CombineGroup(dest, g); SPAssert(groupMap[x][y] == dest); }else{ groupMap[x][y] = dest; } } g2.valid = false; } /** Finds an group in the specified range (tile1, tile2). * If one was not found, creates a new group. * If more than one was found, combines all groups. */ size_t RegisterGroup(IntVector3 tile1, IntVector3 tile2) { size_t foundGroup = NoGroup; for(int x = tile1.x; x < tile2.x; x++) for(int y = tile1.y; y < tile2.y; y++){ size_t g = groupMap[x][y]; if(g == NoGroup) continue; if(g == foundGroup) continue; if(foundGroup == NoGroup) { foundGroup = g; break; } // another group found. // this should be combined with `foundGroup`. CombineGroup(foundGroup, g); SPAssert(groupMap[x][y] == foundGroup); } if(foundGroup == NoGroup) { // new group here. Group g; g.tile1 = tile1; g.tile2 = tile2; g.firstInstance = NoInstance; g.lastInstance = NoInstance; g.lod = -100; //lod is an int g.valid = true; groups.push_back(g); size_t id = groups.size() - 1; FillGroupMap(tile1, tile2, id); return id; }else{ // instance is added to an existing group. bool extended = false; Group& g = groups[foundGroup]; if(tile1.x < g.tile1.x) { extended = true; g.tile1.x = tile1.x; } if(tile1.y < g.tile1.y) { extended = true; g.tile1.y = tile1.y; } if(tile2.x > g.tile2.x) { extended = true; g.tile2.x = tile2.x; } if(tile2.y > g.tile2.y) { extended = true; g.tile2.y = tile2.y; } if(extended) { SPAssert(foundGroup != NoGroup); for(int x = g.tile1.x; x < g.tile2.x; x++) for(int y = g.tile1.y; y < g.tile2.y; y++){ size_t g = groupMap[x][y]; if(g == NoGroup){ groupMap[x][y] = foundGroup; continue; } if(g == foundGroup) continue; // another group found. // this should be combined with `foundGroup`. CombineGroup(foundGroup, g); SPAssert(groupMap[x][y] == foundGroup); } } // don't add instance here return foundGroup; } } Internal(GLSparseShadowMapRenderer *r):renderer(r){ GLProfiler profiler(r->device, "Sparse Page Table Generation"); cameraShadowCoord = r->GetRenderer()->GetSceneDef().viewOrigin; cameraShadowCoord = (r->matrix * cameraShadowCoord).GetXYZ(); // clear group maps for(size_t x = 0; x < Tiles; x++) for(size_t y = 0; y < Tiles; y++) groupMap[x][y] = NoGroup; const std::vector& rmodels = renderer->GetRenderer()->GetModelRenderer()->models; allInstances.reserve(256); groups.reserve(64); nodes.reserve(256); for(std::vector::const_iterator it = rmodels.begin(); it != rmodels.end(); it++) { const GLModelRenderer::RenderModel& rmodel = *it; Instance inst; inst.model = rmodel.model; OBB3 modelBounds = inst.model->GetBoundingBox(); for(size_t i = 0; i < rmodel.params.size(); i++) { inst.param = &(rmodel.params[i]); if(inst.param->depthHack) continue; OBB3 instWorldBoundsOBB = inst.param->matrix * modelBounds; // w should be 1, so this should wor OBB3 instBoundsOBB = r->matrix * instWorldBoundsOBB; AABB3 instBounds = instBoundsOBB.GetBoundingAABB(); // frustrum(?) cull if(instBounds.max.x < -1.f || instBounds.max.y < -1.f || instBounds.min.x > 1.f || instBounds.min.y > 1.f) continue; inst.tile1 = ShadowMapToTileCoord(instBounds.min); inst.tile2 = ShadowMapToTileCoord(instBounds.max); inst.tile2.x++; inst.tile2.y++; if(inst.tile1.x < 0) inst.tile1.x = 0; if(inst.tile1.y < 0) inst.tile1.y = 0; if(inst.tile2.x > Tiles) inst.tile2.x = Tiles; if(inst.tile2.y > Tiles) inst.tile2.y = Tiles; if(inst.tile1.x >= inst.tile2.x) continue; if(inst.tile1.y >= inst.tile2.y) continue; size_t instId = allInstances.size(); size_t grp = RegisterGroup(inst.tile1, inst.tile2); Group& g = groups[grp]; SPAssert(g.valid); if(g.lastInstance == NoInstance) { // this is new group. g.firstInstance = instId; g.lastInstance = instId; inst.prev = NoInstance; inst.next = NoInstance; }else{ // adding this instance to the group Instance& oldLast = allInstances[g.lastInstance]; SPAssert(oldLast.next == NoInstance); oldLast.next = instId; inst.prev = g.lastInstance; inst.next = NoInstance; g.lastInstance = instId; } g.lod = std::max(g.lod, ComputeLod(instBoundsOBB.m.GetOrigin())); allInstances.push_back(inst); } } if(false){ GLProfiler profiler(r->device, "Debug Output"); SPLog("Sparse Page Table -------"); for(int y = 0; y < Tiles; y++) { char buf[Tiles + 1]; for(int x = 0; x < Tiles; x++) { size_t g = groupMap[x][y]; const char *c = "0123456789ABCDEF"; if(g == NoGroup) buf[x] = ' '; else buf[x] = c[g & 15]; } buf[Tiles] = 0; SPLog("%s", buf); } SPLog("-----------------------"); } mapSize = r_shadowMapSize; } bool AddGroupToNode(size_t& nodeRef, int nx, int ny, int nw, int nh, size_t gId) { Group& g = groups[gId]; if(nodeRef == NoNode) { // empty node, try putting here int w = g.tile2.x - g.tile1.x; int h = g.tile2.y - g.tile1.y; int lod = g.lod; int minLod = renderer->minLod; int maxLod = renderer->maxLod; lod += lodBias; if(lod < minLod) lod = minLod; if(lod > maxLod) lod = maxLod; w <<= lod; h <<= lod; w += 2; h += 2; // safety margin if(w > nw || h > nh) { return false; } g.rawX = nx + 1; g.rawY = ny + 1; g.rawW = w-1; g.rawH = h-1; // how fits if(w == nw && h == nh) { // completely fits nodeRef = gId | GroupNodeFlag; }else if(w == nw){ Node node; node.child1 = gId | GroupNodeFlag; node.child2 = NoNode; node.horizontal = false; node.pos = ny + h; nodeRef = nodes.size(); nodes.push_back(node); }else if(h == nh){ Node node; node.child1 = gId | GroupNodeFlag; node.child2 = NoNode; node.horizontal = true; node.pos = nx + w; nodeRef = nodes.size(); nodes.push_back(node); }else{ Node node; node.child1 = gId | GroupNodeFlag; node.child2 = NoNode; node.horizontal = true; node.pos = nx + w; size_t subnode = nodes.size(); nodes.push_back(node); node.child1 = subnode; node.child2 = NoNode; node.horizontal = false; node.pos = ny + h; nodeRef = nodes.size(); nodes.push_back(node); } return true; }else { if(nodeRef & GroupNodeFlag) return false; Node& node = nodes[nodeRef]; if(node.horizontal){ if(AddGroupToNode(node.child1, nx, ny, node.pos - nx, nh, gId)) return true; if(AddGroupToNode(node.child2, node.pos, ny, nx + nw - node.pos, nh, gId)) return true; }else{ if(AddGroupToNode(node.child1, nx, ny, nw, node.pos - ny, gId)) return true; if(AddGroupToNode(node.child2, nx, node.pos, nw, ny + nh - node.pos, gId)) return true; } return false; } } bool TryPack() { size_t rootNode = NoNode; nodes.clear(); for(size_t i = 0; i < groups.size(); i++){ if(!AddGroupToNode(rootNode, 0, 0, mapSize, mapSize, i)) return false; } return true; } void Pack() { if(groups.empty()) return; GLProfiler profiler(renderer->device, "Pack [%d group(s)]", (int)groups.size()); lodBias = 100; if(TryPack()) { // try to make it more detail do{ lodBias++; }while(TryPack() && lodBias < 140); lodBias--; TryPack(); }else { // try to succeed packing by making it more rough do{ lodBias--; }while(!TryPack()); } } }; struct GLSparseShadowMapRenderer::ModelRenderer { std::vector params; GLModel *lastModel; ModelRenderer() { params.resize(64); lastModel = NULL; } void Flush() { if(lastModel){ lastModel->RenderShadowMapPass(params); params.clear(); lastModel = NULL; } } void RenderModel(GLModel *m, const client::ModelRenderParam& param) { if(m != lastModel) Flush(); lastModel = m; params.push_back(param); } }; void GLSparseShadowMapRenderer::RenderShadowMapPass() { Internal itnl(this); itnl.Pack(); { GLProfiler profiler(device, "Page Table Generation"); for(int x = 0; x < Tiles; x++) { for(int y = 0; y < Tiles; y++) { size_t val = itnl.groupMap[x][y]; uint32_t out; if(val == NoGroup){ out = 0xffffffff; }else{ Internal::Group& g = itnl.groups[val]; int lod = g.lod; lod += itnl.lodBias; if(lod < minLod) lod = minLod; if(lod > maxLod) lod = maxLod; int ix = x - g.tile1.x; int iy = y - g.tile1.y; ix <<= lod; iy <<= lod; ix += g.rawX; iy += g.rawY; unsigned int rr, gg, bb, aa; rr = (ix & 255); gg = (iy & 255); bb = (ix >> 8) | ((iy >> 8) << 4); aa = lod;//1 << (lod - minLod); out = bb | (gg << 8) | (rr << 16) | (aa << 24); } pagetable[y][x] = out; } } } { GLProfiler profiler(device, "Page Table Upload"); device->BindTexture(IGLDevice::Texture2D, pagetableTexture); device->TexSubImage2D(IGLDevice::Texture2D, 0, 0, 0, Tiles, Tiles, IGLDevice::BGRA, IGLDevice::UnsignedByte, pagetable); } Matrix4 baseMatrix = matrix; { GLProfiler profiler(device, "Shadow Maps [%d group(s)]", (int)itnl.groups.size()); ModelRenderer mrend; for(size_t i = 0 ; i < itnl.groups.size(); i++) { Internal::Group& g = itnl.groups[i]; device->Viewport(g.rawX, g.rawY, g.rawW, g.rawH); Vector3 dest1 = Internal::TileToShadowMapCoord(g.tile1); Vector3 dest2 = Internal::TileToShadowMapCoord(g.tile2); Matrix4 mat; mat = Matrix4::Translate((dest1.x + dest2.x) * -.5f, (dest1.y + dest2.y) * -.5f, 0.f); mat = Matrix4::Scale(2.f / (dest2.x - dest1.x), 2.f / (dest2.y - dest1.y), 1.f) * mat; mat = mat * baseMatrix; matrix = mat; for(size_t mId = g.firstInstance; mId != NoInstance; mId = itnl.allInstances[mId].next) { Internal::Instance& inst = itnl.allInstances[mId]; mrend.RenderModel(inst.model, *inst.param); Vector3 v = inst.model->GetBoundingBox().min + inst.model->GetBoundingBox().max; v *= .5f; v = (inst.param->matrix * v).GetXYZ(); { v = (baseMatrix * v).GetXYZ(); SPAssert(v.x >= -1.2f); SPAssert(v.y >= -1.2f); SPAssert(v.x <= 1.2f); SPAssert(v.y <= 1.2f); } } mrend.Flush(); } } matrix = baseMatrix; } bool GLSparseShadowMapRenderer::Cull(const spades::AABB3 &){ // TODO: who uses this? SPNotImplemented(); return true; } bool GLSparseShadowMapRenderer::SphereCull(const spades::Vector3 ¢er, float rad){ return true; // for models, this is already done /* Vector4 vw = matrix * center; float xx = fabsf(vw.x); float yy = fabsf(vw.y); float rx = rad * vpWidth; float ry = rad * vpHeight; return xx < (1.f + rx) && yy < (1.f + ry);*/ } } }