1 /* 2 Copyright (c) 2013 yvt 3 4 This file is part of OpenSpades. 5 6 OpenSpades is free software: you can redistribute it and/or modify 7 it under the terms of the GNU General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 OpenSpades is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU General Public License for more details. 15 16 You should have received a copy of the GNU General Public License 17 along with OpenSpades. If not, see <http://www.gnu.org/licenses/>. 18 19 */ 20 21 #include "GLMapRenderer.h" 22 #include <Client/GameMap.h> 23 #include <Core/Debug.h> 24 #include <Core/Settings.h> 25 #include "GLDynamicLightShader.h" 26 #include "GLImage.h" 27 #include "GLMapChunk.h" 28 #include "GLMapShadowRenderer.h" 29 #include "GLProfiler.h" 30 #include "GLProgram.h" 31 #include "GLProgram.h" 32 #include "GLProgramAttribute.h" 33 #include "GLProgramUniform.h" 34 #include "GLRenderer.h" 35 #include "GLShadowShader.h" 36 #include "IGLDevice.h" 37 38 namespace spades { 39 namespace draw { PreloadShaders(spades::draw::GLRenderer * renderer)40 void GLMapRenderer::PreloadShaders(spades::draw::GLRenderer *renderer) { 41 if (renderer->GetSettings().r_physicalLighting) 42 renderer->RegisterProgram("Shaders/BasicBlockPhys.program"); 43 else 44 renderer->RegisterProgram("Shaders/BasicBlock.program"); 45 renderer->RegisterProgram("Shaders/BasicBlockDepthOnly.program"); 46 renderer->RegisterProgram("Shaders/BasicBlockDynamicLit.program"); 47 renderer->RegisterProgram("Shaders/BackFaceBlock.program"); 48 renderer->RegisterImage("Gfx/AmbientOcclusion.png"); 49 } 50 GLMapRenderer(client::GameMap * m,GLRenderer * r)51 GLMapRenderer::GLMapRenderer(client::GameMap *m, GLRenderer *r) : renderer(r), gameMap(m) { 52 SPADES_MARK_FUNCTION(); 53 54 device = renderer->GetGLDevice(); 55 56 numChunkWidth = gameMap->Width() / GLMapChunk::Size; 57 numChunkHeight = gameMap->Height() / GLMapChunk::Size; 58 numChunkDepth = gameMap->Depth() / GLMapChunk::Size; 59 60 numChunks = numChunkWidth * numChunkHeight * numChunkDepth; 61 62 chunks = new GLMapChunk *[numChunks]; 63 chunkInfos = new ChunkRenderInfo[numChunks]; 64 65 for (int i = 0; i < numChunks; i++) 66 chunks[i] = new GLMapChunk(this, gameMap, i / numChunkDepth / numChunkHeight, 67 (i / numChunkDepth) % numChunkHeight, i % numChunkDepth); 68 69 if (r->GetSettings().r_physicalLighting) 70 basicProgram = renderer->RegisterProgram("Shaders/BasicBlockPhys.program"); 71 else 72 basicProgram = renderer->RegisterProgram("Shaders/BasicBlock.program"); 73 depthonlyProgram = renderer->RegisterProgram("Shaders/BasicBlockDepthOnly.program"); 74 dlightProgram = renderer->RegisterProgram("Shaders/BasicBlockDynamicLit.program"); 75 backfaceProgram = renderer->RegisterProgram("Shaders/BackFaceBlock.program"); 76 aoImage = (GLImage *)renderer->RegisterImage("Gfx/AmbientOcclusion.png"); 77 78 static const uint8_t squareVertices[] = {0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1}; 79 squareVertexBuffer = device->GenBuffer(); 80 device->BindBuffer(IGLDevice::ArrayBuffer, squareVertexBuffer); 81 device->BufferData(IGLDevice::ArrayBuffer, sizeof(squareVertices), squareVertices, 82 IGLDevice::StaticDraw); 83 device->BindBuffer(IGLDevice::ArrayBuffer, 0); 84 } 85 ~GLMapRenderer()86 GLMapRenderer::~GLMapRenderer() { 87 SPADES_MARK_FUNCTION(); 88 89 device->DeleteBuffer(squareVertexBuffer); 90 for (int i = 0; i < numChunks; i++) 91 delete chunks[i]; 92 delete[] chunks; 93 delete[] chunkInfos; 94 } GameMapChanged(int x,int y,int z,client::GameMap * map)95 void GLMapRenderer::GameMapChanged(int x, int y, int z, client::GameMap *map) { 96 SPADES_MARK_FUNCTION_DEBUG(); 97 98 /*GetChunk(x >> GLMapChunk::SizeBits, 99 y >> GLMapChunk::SizeBits, 100 z >> GLMapChunk::SizeBits)->SetNeedsUpdate();*/ 101 // int fx = x & (GLMapChunk::Size - 1); 102 // int fy = y & (GLMapChunk::Size - 1); 103 int fz = z & (GLMapChunk::Size - 1); 104 int sx = -1; 105 int sy = -1; 106 int sz = fz == 0 ? -1 : 0; 107 int ex = 1; 108 int ey = 1; 109 int ez = fz == (GLMapChunk::Size - 1) ? 1 : 0; 110 for (int cx = sx; cx <= ex; cx++) 111 for (int cy = sy; cy <= ey; cy++) 112 for (int cz = sz; cz <= ez; cz++) { 113 int xx = x + cx, yy = y + cy, zz = z + cz; 114 xx >>= GLMapChunk::SizeBits; 115 yy >>= GLMapChunk::SizeBits; 116 zz >>= GLMapChunk::SizeBits; 117 xx &= numChunkWidth - 1; 118 yy &= numChunkHeight - 1; 119 if (xx >= 0 && yy >= 0 && zz >= 0 && xx < numChunkWidth && 120 yy < numChunkHeight && zz < numChunkDepth) { 121 GetChunk(xx, yy, zz)->SetNeedsUpdate(); 122 } 123 } 124 } 125 RealizeChunks(spades::Vector3 eye)126 void GLMapRenderer::RealizeChunks(spades::Vector3 eye) { 127 SPADES_MARK_FUNCTION(); 128 129 float cullDistance = 128.f; 130 float releaseDistance = 160.f; 131 for (int i = 0; i < numChunks; i++) { 132 float dist = chunks[i]->DistanceFromEye(eye); 133 chunkInfos[i].distance = dist; 134 if (dist < cullDistance) 135 chunks[i]->SetRealized(true); 136 else if (dist > releaseDistance) 137 chunks[i]->SetRealized(false); 138 } 139 } 140 Realize()141 void GLMapRenderer::Realize() { 142 GLProfiler::Context profiler(renderer->GetGLProfiler(), "Map Chunks"); 143 144 Vector3 eye = renderer->GetSceneDef().viewOrigin; 145 RealizeChunks(eye); 146 } 147 Prerender()148 void GLMapRenderer::Prerender() { 149 SPADES_MARK_FUNCTION(); 150 //depth-only pass 151 152 GLProfiler::Context profiler(renderer->GetGLProfiler(), "Map"); 153 Vector3 eye = renderer->GetSceneDef().viewOrigin; 154 155 device->Enable(IGLDevice::CullFace, true); 156 device->Enable(IGLDevice::DepthTest, true); 157 device->ColorMask(false, false, false, false); 158 159 depthonlyProgram->Use(); 160 static GLProgramAttribute positionAttribute("positionAttribute"); 161 positionAttribute(depthonlyProgram); 162 device->EnableVertexAttribArray(positionAttribute(), true); 163 static GLProgramUniform projectionViewMatrix("projectionViewMatrix"); 164 projectionViewMatrix(depthonlyProgram); 165 projectionViewMatrix.SetValue(renderer->GetProjectionViewMatrix()); 166 167 // draw from nearest to farthest 168 int cx = (int)floorf(eye.x) / GLMapChunk::Size; 169 int cy = (int)floorf(eye.y) / GLMapChunk::Size; 170 int cz = (int)floorf(eye.z) / GLMapChunk::Size; 171 DrawColumnDepth(cx, cy, cz, eye); 172 for (int dist = 1; dist <= 128 / GLMapChunk::Size; dist++) { 173 for (int x = cx - dist; x <= cx + dist; x++) { 174 DrawColumnDepth(x, cy + dist, cz, eye); 175 DrawColumnDepth(x, cy - dist, cz, eye); 176 } 177 for (int y = cy - dist + 1; y <= cy + dist - 1; y++) { 178 DrawColumnDepth(cx + dist, y, cz, eye); 179 DrawColumnDepth(cx - dist, y, cz, eye); 180 } 181 } 182 183 184 device->EnableVertexAttribArray(positionAttribute(), false); 185 device->ColorMask(true, true, true, true); 186 187 } 188 RenderSunlightPass()189 void GLMapRenderer::RenderSunlightPass() { 190 SPADES_MARK_FUNCTION(); 191 192 GLProfiler::Context profiler(renderer->GetGLProfiler(), "Map"); 193 194 Vector3 eye = renderer->GetSceneDef().viewOrigin; 195 196 // draw back face to avoid cheating. 197 // without this, players can see through blocks by 198 // covering themselves by ones. 199 RenderBackface(); 200 201 device->ActiveTexture(0); 202 aoImage->Bind(IGLDevice::Texture2D); 203 device->TexParamater(IGLDevice::Texture2D, IGLDevice::TextureMinFilter, 204 IGLDevice::Linear); 205 206 device->ActiveTexture(1); 207 device->BindTexture(IGLDevice::Texture2D, 0); 208 209 device->Enable(IGLDevice::CullFace, true); 210 device->Enable(IGLDevice::DepthTest, true); 211 212 basicProgram->Use(); 213 214 static GLShadowShader shadowShader; 215 shadowShader(renderer, basicProgram, 2); 216 217 static GLProgramUniform fogDistance("fogDistance"); 218 fogDistance(basicProgram); 219 fogDistance.SetValue(renderer->GetFogDistance()); 220 221 static GLProgramUniform viewSpaceLight("viewSpaceLight"); 222 viewSpaceLight(basicProgram); 223 Vector3 vspLight = (renderer->GetViewMatrix() * MakeVector4(0, -1, -1, 0)).GetXYZ(); 224 viewSpaceLight.SetValue(vspLight.x, vspLight.y, vspLight.z); 225 226 static GLProgramUniform fogColor("fogColor"); 227 fogColor(basicProgram); 228 Vector3 fogCol = renderer->GetFogColorForSolidPass(); 229 fogCol *= fogCol; // linearize 230 fogColor.SetValue(fogCol.x, fogCol.y, fogCol.z); 231 232 static GLProgramUniform aoUniform("ambientOcclusionTexture"); 233 aoUniform(basicProgram); 234 aoUniform.SetValue(0); 235 236 static GLProgramUniform detailTextureUnif("detailTexture"); 237 detailTextureUnif(basicProgram); 238 detailTextureUnif.SetValue(1); 239 240 device->BindBuffer(IGLDevice::ArrayBuffer, 0); 241 242 static GLProgramAttribute positionAttribute("positionAttribute"); 243 static GLProgramAttribute ambientOcclusionCoordAttribute( 244 "ambientOcclusionCoordAttribute"); 245 static GLProgramAttribute colorAttribute("colorAttribute"); 246 static GLProgramAttribute normalAttribute("normalAttribute"); 247 static GLProgramAttribute fixedPositionAttribute("fixedPositionAttribute"); 248 249 positionAttribute(basicProgram); 250 ambientOcclusionCoordAttribute(basicProgram); 251 colorAttribute(basicProgram); 252 normalAttribute(basicProgram); 253 fixedPositionAttribute(basicProgram); 254 255 device->EnableVertexAttribArray(positionAttribute(), true); 256 if (ambientOcclusionCoordAttribute() != -1) 257 device->EnableVertexAttribArray(ambientOcclusionCoordAttribute(), true); 258 device->EnableVertexAttribArray(colorAttribute(), true); 259 if (normalAttribute() != -1) 260 device->EnableVertexAttribArray(normalAttribute(), true); 261 device->EnableVertexAttribArray(fixedPositionAttribute(), true); 262 263 static GLProgramUniform projectionViewMatrix("projectionViewMatrix"); 264 projectionViewMatrix(basicProgram); 265 projectionViewMatrix.SetValue(renderer->GetProjectionViewMatrix()); 266 267 static GLProgramUniform viewMatrix("viewMatrix"); 268 viewMatrix(basicProgram); 269 viewMatrix.SetValue(renderer->GetViewMatrix()); 270 271 static GLProgramUniform viewOriginVector("viewOriginVector"); 272 viewOriginVector(basicProgram); 273 const auto &viewOrigin = renderer->GetSceneDef().viewOrigin; 274 viewOriginVector.SetValue(viewOrigin.x, viewOrigin.y, viewOrigin.z); 275 276 //RealizeChunks(eye); // should already be realized from the prepass 277 //TODO maybe add some way of checking if the chunks have been realized for the current eye? Probably just a bool called "alreadyrealized" that gets checked in RealizeChunks 278 279 // draw from nearest to farthest 280 int cx = (int)floorf(eye.x) / GLMapChunk::Size; 281 int cy = (int)floorf(eye.y) / GLMapChunk::Size; 282 int cz = (int)floorf(eye.z) / GLMapChunk::Size; 283 DrawColumnSunlight(cx, cy, cz, eye); 284 for (int dist = 1; dist <= 128 / GLMapChunk::Size; dist++) { 285 for (int x = cx - dist; x <= cx + dist; x++) { 286 DrawColumnSunlight(x, cy + dist, cz, eye); 287 DrawColumnSunlight(x, cy - dist, cz, eye); 288 } 289 for (int y = cy - dist + 1; y <= cy + dist - 1; y++) { 290 DrawColumnSunlight(cx + dist, y, cz, eye); 291 DrawColumnSunlight(cx - dist, y, cz, eye); 292 } 293 } 294 295 device->EnableVertexAttribArray(positionAttribute(), false); 296 if (ambientOcclusionCoordAttribute() != -1) 297 device->EnableVertexAttribArray(ambientOcclusionCoordAttribute(), false); 298 device->EnableVertexAttribArray(colorAttribute(), false); 299 if (normalAttribute() != -1) 300 device->EnableVertexAttribArray(normalAttribute(), false); 301 device->EnableVertexAttribArray(fixedPositionAttribute(), false); 302 303 device->ActiveTexture(1); 304 device->BindTexture(IGLDevice::Texture2D, 0); 305 device->ActiveTexture(0); 306 device->BindTexture(IGLDevice::Texture2D, 0); 307 } 308 RenderDynamicLightPass(std::vector<GLDynamicLight> lights)309 void GLMapRenderer::RenderDynamicLightPass(std::vector<GLDynamicLight> lights) { 310 SPADES_MARK_FUNCTION(); 311 312 GLProfiler::Context profiler(renderer->GetGLProfiler(), "Map"); 313 314 if (lights.empty()) 315 return; 316 317 Vector3 eye = renderer->GetSceneDef().viewOrigin; 318 319 device->ActiveTexture(0); 320 device->BindTexture(IGLDevice::Texture2D, 0); 321 322 device->Enable(IGLDevice::CullFace, true); 323 device->Enable(IGLDevice::DepthTest, true); 324 325 dlightProgram->Use(); 326 327 static GLProgramUniform fogDistance("fogDistance"); 328 fogDistance(dlightProgram); 329 fogDistance.SetValue(renderer->GetFogDistance()); 330 331 static GLProgramUniform detailTextureUnif("detailTexture"); 332 detailTextureUnif(dlightProgram); 333 detailTextureUnif.SetValue(0); 334 335 device->BindBuffer(IGLDevice::ArrayBuffer, 0); 336 337 static GLProgramAttribute positionAttribute("positionAttribute"); 338 static GLProgramAttribute colorAttribute("colorAttribute"); 339 static GLProgramAttribute normalAttribute("normalAttribute"); 340 341 positionAttribute(dlightProgram); 342 colorAttribute(dlightProgram); 343 normalAttribute(dlightProgram); 344 345 device->EnableVertexAttribArray(positionAttribute(), true); 346 device->EnableVertexAttribArray(colorAttribute(), true); 347 device->EnableVertexAttribArray(normalAttribute(), true); 348 349 static GLProgramUniform projectionViewMatrix("projectionViewMatrix"); 350 projectionViewMatrix(dlightProgram); 351 projectionViewMatrix.SetValue(renderer->GetProjectionViewMatrix()); 352 353 static GLProgramUniform viewMatrix("viewMatrix"); 354 viewMatrix(dlightProgram); 355 viewMatrix.SetValue(renderer->GetViewMatrix()); 356 357 static GLProgramUniform viewOriginVector("viewOriginVector"); 358 viewOriginVector(dlightProgram); 359 const auto &viewOrigin = renderer->GetSceneDef().viewOrigin; 360 viewOriginVector.SetValue(viewOrigin.x, viewOrigin.y, viewOrigin.z); 361 362 //RealizeChunks(eye); // should already be realized from the prepass 363 364 // draw from nearest to farthest 365 int cx = (int)floorf(eye.x) / GLMapChunk::Size; 366 int cy = (int)floorf(eye.y) / GLMapChunk::Size; 367 int cz = (int)floorf(eye.z) / GLMapChunk::Size; 368 DrawColumnDLight(cx, cy, cz, eye, lights); 369 // TODO: optimize call 370 // ex. don't call a chunk'r render method if 371 // no dlight lights it 372 for (int dist = 1; dist <= 128 / GLMapChunk::Size; dist++) { 373 for (int x = cx - dist; x <= cx + dist; x++) { 374 DrawColumnDLight(x, cy + dist, cz, eye, lights); 375 DrawColumnDLight(x, cy - dist, cz, eye, lights); 376 } 377 for (int y = cy - dist + 1; y <= cy + dist - 1; y++) { 378 DrawColumnDLight(cx + dist, y, cz, eye, lights); 379 DrawColumnDLight(cx - dist, y, cz, eye, lights); 380 } 381 } 382 383 device->EnableVertexAttribArray(positionAttribute(), false); 384 device->EnableVertexAttribArray(colorAttribute(), false); 385 device->EnableVertexAttribArray(normalAttribute(), false); 386 387 device->ActiveTexture(0); 388 device->BindTexture(IGLDevice::Texture2D, 0); 389 } 390 DrawColumnDepth(int cx,int cy,int cz,spades::Vector3 eye)391 void GLMapRenderer::DrawColumnDepth(int cx, int cy, int cz, spades::Vector3 eye) { 392 cx &= numChunkWidth - 1; 393 cy &= numChunkHeight - 1; 394 for (int z = std::max(cz, 0); z < numChunkDepth; z++) 395 GetChunk(cx, cy, z)->RenderDepthPass(); 396 for (int z = std::min(cz - 1, 63); z >= 0; z--) 397 GetChunk(cx, cy, z)->RenderDepthPass(); 398 } DrawColumnSunlight(int cx,int cy,int cz,spades::Vector3 eye)399 void GLMapRenderer::DrawColumnSunlight(int cx, int cy, int cz, spades::Vector3 eye) { 400 cx &= numChunkWidth - 1; 401 cy &= numChunkHeight - 1; 402 for (int z = std::max(cz, 0); z < numChunkDepth; z++) 403 GetChunk(cx, cy, z)->RenderSunlightPass(); 404 for (int z = std::min(cz - 1, 63); z >= 0; z--) 405 GetChunk(cx, cy, z)->RenderSunlightPass(); 406 } 407 DrawColumnDLight(int cx,int cy,int cz,spades::Vector3 eye,const std::vector<GLDynamicLight> & lights)408 void GLMapRenderer::DrawColumnDLight(int cx, int cy, int cz, spades::Vector3 eye, 409 const std::vector<GLDynamicLight> &lights) { 410 cx &= numChunkWidth - 1; 411 cy &= numChunkHeight - 1; 412 for (int z = std::max(cz, 0); z < numChunkDepth; z++) 413 GetChunk(cx, cy, z)->RenderDLightPass(lights); 414 for (int z = std::min(cz - 1, 63); z >= 0; z--) 415 GetChunk(cx, cy, z)->RenderDLightPass(lights); 416 } 417 418 #pragma mark - BackFaceBlock 419 420 struct BFVertex { 421 int16_t x, y, z; 422 uint16_t pad; 423 Makespades::draw::BFVertex424 static BFVertex Make(int x, int y, int z) { 425 BFVertex v = {(int16_t)x, (int16_t)y, (int16_t)z, 0}; 426 return v; 427 } 428 }; 429 EmitBackFace(int x,int y,int z,int ux,int uy,int uz,int vx,int vy,int vz,std::vector<BFVertex> & vertices,std::vector<uint16_t> & indices)430 static void EmitBackFace(int x, int y, int z, int ux, int uy, int uz, int vx, int vy, 431 int vz, std::vector<BFVertex> &vertices, 432 std::vector<uint16_t> &indices) { 433 uint16_t idx = (uint16_t)vertices.size(); 434 435 vertices.push_back(BFVertex::Make(x, y, z)); 436 vertices.push_back(BFVertex::Make(x + ux, y + uy, z + uz)); 437 vertices.push_back(BFVertex::Make(x + vx, y + vy, z + vz)); 438 vertices.push_back(BFVertex::Make(x + ux + vx, y + uy + vy, z + uz + vz)); 439 440 indices.push_back(idx); 441 indices.push_back(idx + 1); 442 indices.push_back(idx + 2); 443 indices.push_back(idx + 1); 444 indices.push_back(idx + 3); 445 indices.push_back(idx + 2); 446 } 447 RenderBackface()448 void GLMapRenderer::RenderBackface() { 449 GLProfiler::Context profiler(renderer->GetGLProfiler(), "Back-face"); 450 451 IntVector3 eye = renderer->GetSceneDef().viewOrigin.Floor(); 452 std::vector<BFVertex> vertices; 453 std::vector<uint16_t> indices; 454 client::GameMap *m = gameMap; 455 456 int x, y, z; 457 const int range = 1; 458 for (x = eye.x - range; x <= eye.x + range; x++) { 459 for (y = eye.y - range; y <= eye.y + range; y++) { 460 for (z = eye.z - range; z <= eye.z + range; z++) { 461 if (z >= 63) 462 continue; 463 if (z < 0) 464 continue; 465 if (!m->IsSolidWrapped(x, y, z)) 466 continue; 467 SPAssert(m->IsSolidWrapped(x, y, z)); 468 469 if (m->IsSolidWrapped(x - 1, y, z)) { 470 EmitBackFace(x, y, z, 0, 1, 0, 0, 0, 1, vertices, indices); 471 } 472 if (m->IsSolidWrapped(x + 1, y, z)) { 473 EmitBackFace(x + 1, y, z, 0, 1, 0, 0, 0, 1, vertices, indices); 474 } 475 if (m->IsSolidWrapped(x, y - 1, z)) { 476 EmitBackFace(x, y, z, 1, 0, 0, 0, 0, 1, vertices, indices); 477 } 478 if (m->IsSolidWrapped(x, y + 1, z)) { 479 EmitBackFace(x, y + 1, z, 1, 0, 0, 0, 0, 1, vertices, indices); 480 } 481 if (m->IsSolidWrapped(x, y, z - 1)) { 482 EmitBackFace(x, y, z, 1, 0, 0, 0, 1, 0, vertices, indices); 483 } 484 if (m->IsSolidWrapped(x, y, z + 1)) { 485 EmitBackFace(x, y, z + 1, 1, 0, 0, 0, 1, 0, vertices, indices); 486 } 487 } 488 } 489 } 490 491 if (vertices.empty()) 492 return; 493 494 device->Enable(IGLDevice::CullFace, false); 495 496 backfaceProgram->Use(); 497 498 static GLProgramAttribute positionAttribute("positionAttribute"); 499 static GLProgramUniform projectionViewMatrix("projectionViewMatrix"); 500 501 positionAttribute(backfaceProgram); 502 projectionViewMatrix(backfaceProgram); 503 504 projectionViewMatrix.SetValue(renderer->GetProjectionViewMatrix()); 505 506 device->BindBuffer(IGLDevice::ArrayBuffer, 0); 507 device->VertexAttribPointer(positionAttribute(), 3, IGLDevice::Short, false, 508 sizeof(BFVertex), vertices.data()); 509 510 device->EnableVertexAttribArray(positionAttribute(), true); 511 512 device->BindBuffer(IGLDevice::ElementArrayBuffer, 0); 513 device->DrawElements(IGLDevice::Triangles, 514 static_cast<IGLDevice::Sizei>(indices.size()), 515 IGLDevice::UnsignedShort, indices.data()); 516 517 device->EnableVertexAttribArray(positionAttribute(), false); 518 519 device->Enable(IGLDevice::CullFace, true); 520 } 521 } 522 } 523