1 2/* 3 * Copyright 2011 Google Inc. 4 * 5 * Use of this source code is governed by a BSD-style license that can be 6 * found in the LICENSE file. 7 */ 8 9#include "GrAAHairLinePathRenderer.h" 10 11#include "GrContext.h" 12#include "GrDrawState.h" 13#include "GrGpu.h" 14#include "GrIndexBuffer.h" 15#include "GrPathUtils.h" 16#include "SkGeometry.h" 17#include "SkTemplates.h" 18 19namespace { 20// quadratics are rendered as 5-sided polys in order to bound the 21// AA stroke around the center-curve. See comments in push_quad_index_buffer and 22// bloat_quad. 23static const int kVertsPerQuad = 5; 24static const int kIdxsPerQuad = 9; 25 26static const int kVertsPerLineSeg = 4; 27static const int kIdxsPerLineSeg = 6; 28 29static const int kNumQuadsInIdxBuffer = 256; 30static const size_t kQuadIdxSBufize = kIdxsPerQuad * 31 sizeof(uint16_t) * 32 kNumQuadsInIdxBuffer; 33 34bool push_quad_index_data(GrIndexBuffer* qIdxBuffer) { 35 uint16_t* data = (uint16_t*) qIdxBuffer->lock(); 36 bool tempData = NULL == data; 37 if (tempData) { 38 data = new uint16_t[kNumQuadsInIdxBuffer * kIdxsPerQuad]; 39 } 40 for (int i = 0; i < kNumQuadsInIdxBuffer; ++i) { 41 42 // Each quadratic is rendered as a five sided polygon. This poly bounds 43 // the quadratic's bounding triangle but has been expanded so that the 44 // 1-pixel wide area around the curve is inside the poly. 45 // If a,b,c are the original control points then the poly a0,b0,c0,c1,a1 46 // that is rendered would look like this: 47 // b0 48 // b 49 // 50 // a0 c0 51 // a c 52 // a1 c1 53 // Each is drawn as three triagnles specified by these 9 indices: 54 int baseIdx = i * kIdxsPerQuad; 55 uint16_t baseVert = (uint16_t)(i * kVertsPerQuad); 56 data[0 + baseIdx] = baseVert + 0; // a0 57 data[1 + baseIdx] = baseVert + 1; // a1 58 data[2 + baseIdx] = baseVert + 2; // b0 59 data[3 + baseIdx] = baseVert + 2; // b0 60 data[4 + baseIdx] = baseVert + 4; // c1 61 data[5 + baseIdx] = baseVert + 3; // c0 62 data[6 + baseIdx] = baseVert + 1; // a1 63 data[7 + baseIdx] = baseVert + 4; // c1 64 data[8 + baseIdx] = baseVert + 2; // b0 65 } 66 if (tempData) { 67 bool ret = qIdxBuffer->updateData(data, kQuadIdxSBufize); 68 delete[] data; 69 return ret; 70 } else { 71 qIdxBuffer->unlock(); 72 return true; 73 } 74} 75} 76 77GrPathRenderer* GrAAHairLinePathRenderer::Create(GrContext* context) { 78 const GrIndexBuffer* lIdxBuffer = context->getQuadIndexBuffer(); 79 if (NULL == lIdxBuffer) { 80 return NULL; 81 } 82 GrGpu* gpu = context->getGpu(); 83 GrIndexBuffer* qIdxBuf = gpu->createIndexBuffer(kQuadIdxSBufize, false); 84 SkAutoTUnref<GrIndexBuffer> qIdxBuffer(qIdxBuf); 85 if (NULL == qIdxBuf || 86 !push_quad_index_data(qIdxBuf)) { 87 return NULL; 88 } 89 return new GrAAHairLinePathRenderer(context, 90 lIdxBuffer, 91 qIdxBuf); 92} 93 94GrAAHairLinePathRenderer::GrAAHairLinePathRenderer( 95 const GrContext* context, 96 const GrIndexBuffer* linesIndexBuffer, 97 const GrIndexBuffer* quadsIndexBuffer) { 98 fLinesIndexBuffer = linesIndexBuffer; 99 linesIndexBuffer->ref(); 100 fQuadsIndexBuffer = quadsIndexBuffer; 101 quadsIndexBuffer->ref(); 102} 103 104GrAAHairLinePathRenderer::~GrAAHairLinePathRenderer() { 105 fLinesIndexBuffer->unref(); 106 fQuadsIndexBuffer->unref(); 107} 108 109namespace { 110 111typedef SkTArray<SkPoint, true> PtArray; 112#define PREALLOC_PTARRAY(N) SkSTArray<(N),SkPoint, true> 113typedef SkTArray<int, true> IntArray; 114 115// Takes 178th time of logf on Z600 / VC2010 116int get_float_exp(float x) { 117 GR_STATIC_ASSERT(sizeof(int) == sizeof(float)); 118#if GR_DEBUG 119 static bool tested; 120 if (!tested) { 121 tested = true; 122 GrAssert(get_float_exp(0.25f) == -2); 123 GrAssert(get_float_exp(0.3f) == -2); 124 GrAssert(get_float_exp(0.5f) == -1); 125 GrAssert(get_float_exp(1.f) == 0); 126 GrAssert(get_float_exp(2.f) == 1); 127 GrAssert(get_float_exp(2.5f) == 1); 128 GrAssert(get_float_exp(8.f) == 3); 129 GrAssert(get_float_exp(100.f) == 6); 130 GrAssert(get_float_exp(1000.f) == 9); 131 GrAssert(get_float_exp(1024.f) == 10); 132 GrAssert(get_float_exp(3000000.f) == 21); 133 } 134#endif 135 const int* iptr = (const int*)&x; 136 return (((*iptr) & 0x7f800000) >> 23) - 127; 137} 138 139// we subdivide the quads to avoid huge overfill 140// if it returns -1 then should be drawn as lines 141int num_quad_subdivs(const SkPoint p[3]) { 142 static const SkScalar gDegenerateToLineTol = SK_Scalar1; 143 static const SkScalar gDegenerateToLineTolSqd = 144 SkScalarMul(gDegenerateToLineTol, gDegenerateToLineTol); 145 146 if (p[0].distanceToSqd(p[1]) < gDegenerateToLineTolSqd || 147 p[1].distanceToSqd(p[2]) < gDegenerateToLineTolSqd) { 148 return -1; 149 } 150 151 GrScalar dsqd = p[1].distanceToLineBetweenSqd(p[0], p[2]); 152 if (dsqd < gDegenerateToLineTolSqd) { 153 return -1; 154 } 155 156 if (p[2].distanceToLineBetweenSqd(p[1], p[0]) < gDegenerateToLineTolSqd) { 157 return -1; 158 } 159 160 static const int kMaxSub = 4; 161 // tolerance of triangle height in pixels 162 // tuned on windows Quadro FX 380 / Z600 163 // trade off of fill vs cpu time on verts 164 // maybe different when do this using gpu (geo or tess shaders) 165 static const SkScalar gSubdivTol = 175 * SK_Scalar1; 166 167 if (dsqd <= gSubdivTol*gSubdivTol) { 168 return 0; 169 } else { 170 // subdividing the quad reduces d by 4. so we want x = log4(d/tol) 171 // = log4(d*d/tol*tol)/2 172 // = log2(d*d/tol*tol) 173 174#ifdef SK_SCALAR_IS_FLOAT 175 // +1 since we're ignoring the mantissa contribution. 176 int log = get_float_exp(dsqd/(gSubdivTol*gSubdivTol)) + 1; 177 log = GrMin(GrMax(0, log), kMaxSub); 178 return log; 179#else 180 SkScalar log = SkScalarLog(SkScalarDiv(dsqd,gSubdivTol*gSubdivTol)); 181 static const SkScalar conv = SkScalarInvert(SkScalarLog(2)); 182 log = SkScalarMul(log, conv); 183 return GrMin(GrMax(0, SkScalarCeilToInt(log)),kMaxSub); 184#endif 185 } 186} 187 188/** 189 * Generates the lines and quads to be rendered. Lines are always recorded in 190 * device space. We will do a device space bloat to account for the 1pixel 191 * thickness. 192 * Quads are recorded in device space unless m contains 193 * perspective, then in they are in src space. We do this because we will 194 * subdivide large quads to reduce over-fill. This subdivision has to be 195 * performed before applying the perspective matrix. 196 */ 197int generate_lines_and_quads(const SkPath& path, 198 const SkMatrix& m, 199 const SkVector& translate, 200 GrIRect clip, 201 PtArray* lines, 202 PtArray* quads, 203 IntArray* quadSubdivCnts) { 204 SkPath::Iter iter(path, false); 205 206 int totalQuadCount = 0; 207 GrRect bounds; 208 GrIRect ibounds; 209 210 bool persp = m.hasPerspective(); 211 212 for (;;) { 213 GrPoint pts[4]; 214 GrPoint devPts[4]; 215 GrPathCmd cmd = (GrPathCmd)iter.next(pts); 216 switch (cmd) { 217 case kMove_PathCmd: 218 break; 219 case kLine_PathCmd: 220 SkPoint::Offset(pts, 2, translate); 221 m.mapPoints(devPts, pts, 2); 222 bounds.setBounds(devPts, 2); 223 bounds.outset(SK_Scalar1, SK_Scalar1); 224 bounds.roundOut(&ibounds); 225 if (SkIRect::Intersects(clip, ibounds)) { 226 SkPoint* pts = lines->push_back_n(2); 227 pts[0] = devPts[0]; 228 pts[1] = devPts[1]; 229 } 230 break; 231 case kQuadratic_PathCmd: 232 SkPoint::Offset(pts, 3, translate); 233 m.mapPoints(devPts, pts, 3); 234 bounds.setBounds(devPts, 3); 235 bounds.outset(SK_Scalar1, SK_Scalar1); 236 bounds.roundOut(&ibounds); 237 if (SkIRect::Intersects(clip, ibounds)) { 238 int subdiv = num_quad_subdivs(devPts); 239 GrAssert(subdiv >= -1); 240 if (-1 == subdiv) { 241 SkPoint* pts = lines->push_back_n(4); 242 pts[0] = devPts[0]; 243 pts[1] = devPts[1]; 244 pts[2] = devPts[1]; 245 pts[3] = devPts[2]; 246 } else { 247 // when in perspective keep quads in src space 248 SkPoint* qPts = persp ? pts : devPts; 249 SkPoint* pts = quads->push_back_n(3); 250 pts[0] = qPts[0]; 251 pts[1] = qPts[1]; 252 pts[2] = qPts[2]; 253 quadSubdivCnts->push_back() = subdiv; 254 totalQuadCount += 1 << subdiv; 255 } 256 } 257 break; 258 case kCubic_PathCmd: 259 SkPoint::Offset(pts, 4, translate); 260 m.mapPoints(devPts, pts, 4); 261 bounds.setBounds(devPts, 4); 262 bounds.outset(SK_Scalar1, SK_Scalar1); 263 bounds.roundOut(&ibounds); 264 if (SkIRect::Intersects(clip, ibounds)) { 265 PREALLOC_PTARRAY(32) q; 266 // We convert cubics to quadratics (for now). 267 // In perspective have to do conversion in src space. 268 if (persp) { 269 SkScalar tolScale = 270 GrPathUtils::scaleToleranceToSrc(SK_Scalar1, m, 271 path.getBounds()); 272 GrPathUtils::convertCubicToQuads(pts, tolScale, &q); 273 } else { 274 GrPathUtils::convertCubicToQuads(devPts, SK_Scalar1, &q); 275 } 276 for (int i = 0; i < q.count(); i += 3) { 277 SkPoint* qInDevSpace; 278 // bounds has to be calculated in device space, but q is 279 // in src space when there is perspective. 280 if (persp) { 281 m.mapPoints(devPts, &q[i], 3); 282 bounds.setBounds(devPts, 3); 283 qInDevSpace = devPts; 284 } else { 285 bounds.setBounds(&q[i], 3); 286 qInDevSpace = &q[i]; 287 } 288 bounds.outset(SK_Scalar1, SK_Scalar1); 289 bounds.roundOut(&ibounds); 290 if (SkIRect::Intersects(clip, ibounds)) { 291 int subdiv = num_quad_subdivs(qInDevSpace); 292 GrAssert(subdiv >= -1); 293 if (-1 == subdiv) { 294 SkPoint* pts = lines->push_back_n(4); 295 // lines should always be in device coords 296 pts[0] = qInDevSpace[0]; 297 pts[1] = qInDevSpace[1]; 298 pts[2] = qInDevSpace[1]; 299 pts[3] = qInDevSpace[2]; 300 } else { 301 SkPoint* pts = quads->push_back_n(3); 302 // q is already in src space when there is no 303 // perspective and dev coords otherwise. 304 pts[0] = q[0 + i]; 305 pts[1] = q[1 + i]; 306 pts[2] = q[2 + i]; 307 quadSubdivCnts->push_back() = subdiv; 308 totalQuadCount += 1 << subdiv; 309 } 310 } 311 } 312 } 313 break; 314 case kClose_PathCmd: 315 break; 316 case kEnd_PathCmd: 317 return totalQuadCount; 318 } 319 } 320} 321 322struct Vertex { 323 GrPoint fPos; 324 union { 325 struct { 326 GrScalar fA; 327 GrScalar fB; 328 GrScalar fC; 329 } fLine; 330 GrVec fQuadCoord; 331 struct { 332 GrScalar fBogus[4]; 333 }; 334 }; 335}; 336GR_STATIC_ASSERT(sizeof(Vertex) == 3 * sizeof(GrPoint)); 337 338void intersect_lines(const SkPoint& ptA, const SkVector& normA, 339 const SkPoint& ptB, const SkVector& normB, 340 SkPoint* result) { 341 342 SkScalar lineAW = -normA.dot(ptA); 343 SkScalar lineBW = -normB.dot(ptB); 344 345 SkScalar wInv = SkScalarMul(normA.fX, normB.fY) - 346 SkScalarMul(normA.fY, normB.fX); 347 wInv = SkScalarInvert(wInv); 348 349 result->fX = SkScalarMul(normA.fY, lineBW) - SkScalarMul(lineAW, normB.fY); 350 result->fX = SkScalarMul(result->fX, wInv); 351 352 result->fY = SkScalarMul(lineAW, normB.fX) - SkScalarMul(normA.fX, lineBW); 353 result->fY = SkScalarMul(result->fY, wInv); 354} 355 356void bloat_quad(const SkPoint qpts[3], const GrMatrix* toDevice, 357 const GrMatrix* toSrc, Vertex verts[kVertsPerQuad]) { 358 GrAssert(!toDevice == !toSrc); 359 // original quad is specified by tri a,b,c 360 SkPoint a = qpts[0]; 361 SkPoint b = qpts[1]; 362 SkPoint c = qpts[2]; 363 364 // this should be in the src space, not dev coords, when we have perspective 365 SkMatrix DevToUV; 366 GrPathUtils::quadDesignSpaceToUVCoordsMatrix(qpts, &DevToUV); 367 368 if (toDevice) { 369 toDevice->mapPoints(&a, 1); 370 toDevice->mapPoints(&b, 1); 371 toDevice->mapPoints(&c, 1); 372 } 373 // make a new poly where we replace a and c by a 1-pixel wide edges orthog 374 // to edges ab and bc: 375 // 376 // before | after 377 // | b0 378 // b | 379 // | 380 // | a0 c0 381 // a c | a1 c1 382 // 383 // edges a0->b0 and b0->c0 are parallel to original edges a->b and b->c, 384 // respectively. 385 Vertex& a0 = verts[0]; 386 Vertex& a1 = verts[1]; 387 Vertex& b0 = verts[2]; 388 Vertex& c0 = verts[3]; 389 Vertex& c1 = verts[4]; 390 391 SkVector ab = b; 392 ab -= a; 393 SkVector ac = c; 394 ac -= a; 395 SkVector cb = b; 396 cb -= c; 397 398 // We should have already handled degenerates 399 GrAssert(ab.length() > 0 && cb.length() > 0); 400 401 ab.normalize(); 402 SkVector abN; 403 abN.setOrthog(ab, SkVector::kLeft_Side); 404 if (abN.dot(ac) > 0) { 405 abN.negate(); 406 } 407 408 cb.normalize(); 409 SkVector cbN; 410 cbN.setOrthog(cb, SkVector::kLeft_Side); 411 if (cbN.dot(ac) < 0) { 412 cbN.negate(); 413 } 414 415 a0.fPos = a; 416 a0.fPos += abN; 417 a1.fPos = a; 418 a1.fPos -= abN; 419 420 c0.fPos = c; 421 c0.fPos += cbN; 422 c1.fPos = c; 423 c1.fPos -= cbN; 424 425 intersect_lines(a0.fPos, abN, c0.fPos, cbN, &b0.fPos); 426 427 if (toSrc) { 428 toSrc->mapPointsWithStride(&verts[0].fPos, sizeof(Vertex), kVertsPerQuad); 429 } 430 DevToUV.mapPointsWithStride(&verts[0].fQuadCoord, 431 &verts[0].fPos, sizeof(Vertex), kVertsPerQuad); 432} 433 434void add_quads(const SkPoint p[3], 435 int subdiv, 436 const GrMatrix* toDevice, 437 const GrMatrix* toSrc, 438 Vertex** vert) { 439 GrAssert(subdiv >= 0); 440 if (subdiv) { 441 SkPoint newP[5]; 442 SkChopQuadAtHalf(p, newP); 443 add_quads(newP + 0, subdiv-1, toDevice, toSrc, vert); 444 add_quads(newP + 2, subdiv-1, toDevice, toSrc, vert); 445 } else { 446 bloat_quad(p, toDevice, toSrc, *vert); 447 *vert += kVertsPerQuad; 448 } 449} 450 451void add_line(const SkPoint p[2], 452 int rtHeight, 453 const SkMatrix* toSrc, 454 Vertex** vert) { 455 const SkPoint& a = p[0]; 456 const SkPoint& b = p[1]; 457 458 SkVector orthVec = b; 459 orthVec -= a; 460 461 if (orthVec.setLength(SK_Scalar1)) { 462 orthVec.setOrthog(orthVec); 463 464 // the values we pass down to the frag shader 465 // have to be in y-points-up space; 466 SkVector normal; 467 normal.fX = orthVec.fX; 468 normal.fY = -orthVec.fY; 469 SkPoint aYDown; 470 aYDown.fX = a.fX; 471 aYDown.fY = rtHeight - a.fY; 472 473 SkScalar lineC = -(aYDown.dot(normal)); 474 for (int i = 0; i < kVertsPerLineSeg; ++i) { 475 (*vert)[i].fPos = (i < 2) ? a : b; 476 if (0 == i || 3 == i) { 477 (*vert)[i].fPos -= orthVec; 478 } else { 479 (*vert)[i].fPos += orthVec; 480 } 481 (*vert)[i].fLine.fA = normal.fX; 482 (*vert)[i].fLine.fB = normal.fY; 483 (*vert)[i].fLine.fC = lineC; 484 } 485 if (NULL != toSrc) { 486 toSrc->mapPointsWithStride(&(*vert)->fPos, 487 sizeof(Vertex), 488 kVertsPerLineSeg); 489 } 490 } else { 491 // just make it degenerate and likely offscreen 492 (*vert)[0].fPos.set(SK_ScalarMax, SK_ScalarMax); 493 (*vert)[1].fPos.set(SK_ScalarMax, SK_ScalarMax); 494 (*vert)[2].fPos.set(SK_ScalarMax, SK_ScalarMax); 495 (*vert)[3].fPos.set(SK_ScalarMax, SK_ScalarMax); 496 } 497 498 *vert += kVertsPerLineSeg; 499} 500 501} 502 503bool GrAAHairLinePathRenderer::createGeom(const SkPath& path, 504 const GrVec* translate, 505 GrDrawTarget* target, 506 GrDrawState::StageMask stageMask, 507 int* lineCnt, 508 int* quadCnt) { 509 const GrDrawState& drawState = target->getDrawState(); 510 int rtHeight = drawState.getRenderTarget()->height(); 511 512 GrIRect clip; 513 if (target->getClip().hasConservativeBounds()) { 514 GrRect clipRect = target->getClip().getConservativeBounds(); 515 clipRect.roundOut(&clip); 516 } else { 517 clip.setLargest(); 518 } 519 520 521 GrVertexLayout layout = GrDrawTarget::kEdge_VertexLayoutBit; 522 for (int s = 0; s < GrDrawState::kNumStages; ++s) { 523 if ((1 << s) & stageMask) { 524 layout |= GrDrawTarget::StagePosAsTexCoordVertexLayoutBit(s); 525 } 526 } 527 528 GrMatrix viewM = drawState.getViewMatrix(); 529 530 PREALLOC_PTARRAY(128) lines; 531 PREALLOC_PTARRAY(128) quads; 532 IntArray qSubdivs; 533 static const GrVec gZeroVec = {0, 0}; 534 if (NULL == translate) { 535 translate = &gZeroVec; 536 } 537 *quadCnt = generate_lines_and_quads(path, viewM, *translate, clip, 538 &lines, &quads, &qSubdivs); 539 540 *lineCnt = lines.count() / 2; 541 int vertCnt = kVertsPerLineSeg * *lineCnt + kVertsPerQuad * *quadCnt; 542 543 GrAssert(sizeof(Vertex) == GrDrawTarget::VertexSize(layout)); 544 545 Vertex* verts; 546 if (!target->reserveVertexSpace(layout, vertCnt, (void**)&verts)) { 547 return false; 548 } 549 550 const GrMatrix* toDevice = NULL; 551 const GrMatrix* toSrc = NULL; 552 GrMatrix ivm; 553 554 if (viewM.hasPerspective()) { 555 if (viewM.invert(&ivm)) { 556 toDevice = &viewM; 557 toSrc = &ivm; 558 } 559 } 560 561 for (int i = 0; i < *lineCnt; ++i) { 562 add_line(&lines[2*i], rtHeight, toSrc, &verts); 563 } 564 565 int unsubdivQuadCnt = quads.count() / 3; 566 for (int i = 0; i < unsubdivQuadCnt; ++i) { 567 GrAssert(qSubdivs[i] >= 0); 568 add_quads(&quads[3*i], qSubdivs[i], toDevice, toSrc, &verts); 569 } 570 571 return true; 572} 573 574bool GrAAHairLinePathRenderer::canDrawPath(const SkPath& path, 575 GrPathFill fill, 576 const GrDrawTarget* target, 577 bool antiAlias) const { 578 if (fill != kHairLine_PathFill || !antiAlias) { 579 return false; 580 } 581 582 static const uint32_t gReqDerivMask = SkPath::kCubic_SegmentMask | 583 SkPath::kQuad_SegmentMask; 584 if (!target->getCaps().fShaderDerivativeSupport && 585 (gReqDerivMask & path.getSegmentMasks())) { 586 return false; 587 } 588 return true; 589} 590 591bool GrAAHairLinePathRenderer::onDrawPath(const SkPath& path, 592 GrPathFill fill, 593 const GrVec* translate, 594 GrDrawTarget* target, 595 GrDrawState::StageMask stageMask, 596 bool antiAlias) { 597 598 int lineCnt; 599 int quadCnt; 600 601 if (!this->createGeom(path, 602 translate, 603 target, 604 stageMask, 605 &lineCnt, 606 &quadCnt)) { 607 return false; 608 } 609 610 GrDrawState* drawState = target->drawState(); 611 612 GrDrawTarget::AutoStateRestore asr; 613 if (!drawState->getViewMatrix().hasPerspective()) { 614 asr.set(target); 615 GrMatrix ivm; 616 if (drawState->getViewInverse(&ivm)) { 617 drawState->preConcatSamplerMatrices(stageMask, ivm); 618 } 619 drawState->setViewMatrix(GrMatrix::I()); 620 } 621 622 // TODO: See whether rendering lines as degenerate quads improves perf 623 // when we have a mix 624 target->setIndexSourceToBuffer(fLinesIndexBuffer); 625 int lines = 0; 626 int nBufLines = fLinesIndexBuffer->maxQuads(); 627 while (lines < lineCnt) { 628 int n = GrMin(lineCnt - lines, nBufLines); 629 drawState->setVertexEdgeType(GrDrawState::kHairLine_EdgeType); 630 target->drawIndexed(kTriangles_PrimitiveType, 631 kVertsPerLineSeg*lines, // startV 632 0, // startI 633 kVertsPerLineSeg*n, // vCount 634 kIdxsPerLineSeg*n); // iCount 635 lines += n; 636 } 637 638 target->setIndexSourceToBuffer(fQuadsIndexBuffer); 639 int quads = 0; 640 while (quads < quadCnt) { 641 int n = GrMin(quadCnt - quads, kNumQuadsInIdxBuffer); 642 drawState->setVertexEdgeType(GrDrawState::kHairQuad_EdgeType); 643 target->drawIndexed(kTriangles_PrimitiveType, 644 4 * lineCnt + kVertsPerQuad*quads, // startV 645 0, // startI 646 kVertsPerQuad*n, // vCount 647 kIdxsPerQuad*n); // iCount 648 quads += n; 649 } 650 return true; 651} 652 653