1 /* 2 Inochi2D Part 3 4 Copyright © 2020, Inochi2D Project 5 Distributed under the 2-Clause BSD License, see LICENSE file. 6 7 Authors: Luna Nielsen 8 */ 9 module inochi2d.core.nodes.part; 10 import inochi2d.integration; 11 import inochi2d.fmt; 12 import inochi2d.core.nodes.drawable; 13 import inochi2d.core; 14 import inochi2d.math; 15 import bindbc.opengl; 16 import std.exception; 17 import std.algorithm.mutation : copy; 18 public import inochi2d.core.nodes.common; 19 import std.math : isNaN; 20 21 public import inochi2d.core.meshdata; 22 23 24 package(inochi2d) { 25 private { 26 Shader partShader; 27 Shader partMaskShader; 28 29 /* GLSL Uniforms (Normal) */ 30 GLint mvp; 31 GLint offset; 32 GLint gopacity; 33 GLint gMultColor; 34 GLint gScreenColor; 35 36 /* GLSL Uniforms (Masks) */ 37 GLint mmvp; 38 GLint mthreshold; 39 40 GLuint sVertexBuffer; 41 GLuint sUVBuffer; 42 GLuint sElementBuffer; 43 } 44 45 void inInitPart() { 46 inRegisterNodeType!Part; 47 48 version(InDoesRender) { 49 partShader = new Shader(import("basic/basic.vert"), import("basic/basic.frag")); 50 partMaskShader = new Shader(import("basic/basic.vert"), import("basic/basic-mask.frag")); 51 52 mvp = partShader.getUniformLocation("mvp"); 53 offset = partShader.getUniformLocation("offset"); 54 gopacity = partShader.getUniformLocation("opacity"); 55 gMultColor = partShader.getUniformLocation("multColor"); 56 gScreenColor = partShader.getUniformLocation("screenColor"); 57 58 mmvp = partMaskShader.getUniformLocation("mvp"); 59 mthreshold = partMaskShader.getUniformLocation("threshold"); 60 61 glGenBuffers(1, &sVertexBuffer); 62 glGenBuffers(1, &sUVBuffer); 63 glGenBuffers(1, &sElementBuffer); 64 } 65 } 66 } 67 68 69 /** 70 Creates a simple part that is sized after the texture given 71 part is created based on file path given. 72 Supported file types are: png, tga and jpeg 73 74 This is unoptimal for normal use and should only be used 75 for real-time use when you want to add/remove parts on the fly 76 */ 77 Part inCreateSimplePart(string file, Node parent = null) { 78 return inCreateSimplePart(ShallowTexture(file), parent, file); 79 } 80 81 /** 82 Creates a simple part that is sized after the texture given 83 84 This is unoptimal for normal use and should only be used 85 for real-time use when you want to add/remove parts on the fly 86 */ 87 Part inCreateSimplePart(ShallowTexture texture, Node parent = null, string name = "New Part") { 88 return inCreateSimplePart(new Texture(texture), parent, name); 89 } 90 91 /** 92 Creates a simple part that is sized after the texture given 93 94 This is unoptimal for normal use and should only be used 95 for real-time use when you want to add/remove parts on the fly 96 */ 97 Part inCreateSimplePart(Texture tex, Node parent = null, string name = "New Part") { 98 MeshData data = MeshData([ 99 vec2(-(tex.width/2), -(tex.height/2)), 100 vec2(-(tex.width/2), tex.height/2), 101 vec2(tex.width/2, -(tex.height/2)), 102 vec2(tex.width/2, tex.height/2), 103 ], 104 [ 105 vec2(0, 0), 106 vec2(0, 1), 107 vec2(1, 0), 108 vec2(1, 1), 109 ], 110 [ 111 0, 1, 2, 112 2, 1, 3 113 ]); 114 Part p = new Part(data, [tex], parent); 115 p.name = name; 116 return p; 117 } 118 119 /** 120 Dynamic Mesh Part 121 */ 122 @TypeId("Part") 123 class Part : Drawable { 124 private: 125 126 GLuint uvbo; 127 128 void updateUVs() { 129 version(InDoesRender) { 130 glBindBuffer(GL_ARRAY_BUFFER, uvbo); 131 glBufferData(GL_ARRAY_BUFFER, data.uvs.length*vec2.sizeof, data.uvs.ptr, GL_STATIC_DRAW); 132 } 133 } 134 135 /* 136 RENDERING 137 */ 138 139 void drawSelf(bool isMask = false)() { 140 141 // In some cases this may happen 142 if (textures.length == 0) return; 143 144 // Bind the vertex array 145 incDrawableBindVAO(); 146 147 static if (isMask) { 148 partMaskShader.use(); 149 partMaskShader.setUniform(offset, data.origin); 150 partMaskShader.setUniform(mmvp, inGetCamera().matrix * transform.matrix()); 151 partMaskShader.setUniform(mthreshold, clamp(offsetMaskThreshold + maskAlphaThreshold, 0, 1)); 152 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); 153 } else { 154 partShader.use(); 155 partShader.setUniform(offset, data.origin); 156 partShader.setUniform(mvp, inGetCamera().matrix * transform.matrix()); 157 partShader.setUniform(gopacity, clamp(offsetOpacity * opacity, 0, 1)); 158 159 vec3 clampedColor = tint; 160 if (!offsetTint.x.isNaN) clampedColor.x = clamp(tint.x*offsetTint.x, 0, 1); 161 if (!offsetTint.y.isNaN) clampedColor.y = clamp(tint.y*offsetTint.y, 0, 1); 162 if (!offsetTint.z.isNaN) clampedColor.z = clamp(tint.z*offsetTint.z, 0, 1); 163 partShader.setUniform(gMultColor, clampedColor); 164 165 clampedColor = screenTint; 166 if (!offsetScreenTint.x.isNaN) clampedColor.x = clamp(screenTint.x+offsetScreenTint.x, 0, 1); 167 if (!offsetScreenTint.y.isNaN) clampedColor.y = clamp(screenTint.y+offsetScreenTint.y, 0, 1); 168 if (!offsetScreenTint.z.isNaN) clampedColor.z = clamp(screenTint.z+offsetScreenTint.z, 0, 1); 169 partShader.setUniform(gScreenColor, clampedColor); 170 inSetBlendMode(blendingMode); 171 172 // TODO: EXT MODE 173 } 174 175 // Bind the texture 176 textures[0].bind(); 177 178 // Enable points array 179 glEnableVertexAttribArray(0); 180 glBindBuffer(GL_ARRAY_BUFFER, vbo); 181 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 182 183 // Enable UVs array 184 glEnableVertexAttribArray(1); // uvs 185 glBindBuffer(GL_ARRAY_BUFFER, uvbo); 186 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, null); 187 188 // Enable deform array 189 glEnableVertexAttribArray(2); // deforms 190 glBindBuffer(GL_ARRAY_BUFFER, dbo); 191 glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, null); 192 193 // Bind index buffer 194 this.bindIndex(); 195 196 // Disable the vertex attribs after use 197 glDisableVertexAttribArray(0); 198 glDisableVertexAttribArray(1); 199 glDisableVertexAttribArray(2); 200 } 201 202 protected: 203 override 204 void renderMask(bool dodge = false) { 205 206 // Enable writing to stencil buffer and disable writing to color buffer 207 glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); 208 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); 209 glStencilFunc(GL_ALWAYS, dodge ? 0 : 1, 0xFF); 210 glStencilMask(0xFF); 211 212 // Draw ourselves to the stencil buffer 213 drawSelf!true(); 214 215 // Disable writing to stencil buffer and enable writing to color buffer 216 glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); 217 } 218 219 override 220 string typeId() { return "Part"; } 221 222 /** 223 Allows serializing self data (with pretty serializer) 224 */ 225 override 226 void serializeSelf(ref InochiSerializer serializer) { 227 super.serializeSelf(serializer); 228 version (InDoesRender) { 229 if (inIsINPMode()) { 230 serializer.putKey("textures"); 231 auto state = serializer.arrayBegin(); 232 foreach(texture; textures) { 233 ptrdiff_t index = puppet.getTextureSlotIndexFor(texture); 234 if (index >= 0) { 235 serializer.elemBegin; 236 serializer.putValue(cast(size_t)index); 237 } 238 } 239 serializer.arrayEnd(state); 240 } else { 241 serializer.putKey("textures"); 242 auto state = serializer.arrayBegin(); 243 serializer.elemBegin; 244 serializer.putValue(name); 245 serializer.arrayEnd(state); 246 } 247 } 248 249 serializer.putKey("blend_mode"); 250 serializer.serializeValue(blendingMode); 251 252 serializer.putKey("tint"); 253 tint.serialize(serializer); 254 255 serializer.putKey("screenTint"); 256 screenTint.serialize(serializer); 257 258 if (masks.length > 0) { 259 serializer.putKey("masks"); 260 auto state = serializer.arrayBegin(); 261 foreach(m; masks) { 262 serializer.elemBegin; 263 serializer.serializeValue(m); 264 } 265 serializer.arrayEnd(state); 266 } 267 268 serializer.putKey("mask_threshold"); 269 serializer.putValue(maskAlphaThreshold); 270 271 serializer.putKey("opacity"); 272 serializer.putValue(opacity); 273 } 274 275 /** 276 Allows serializing self data (with compact serializer) 277 */ 278 override 279 void serializeSelf(ref InochiSerializerCompact serializer) { 280 super.serializeSelf(serializer); 281 282 if (inIsINPMode()) { 283 serializer.putKey("textures"); 284 auto state = serializer.arrayBegin(); 285 foreach(texture; textures) { 286 ptrdiff_t index = puppet.getTextureSlotIndexFor(texture); 287 if (index >= 0) { 288 serializer.elemBegin; 289 serializer.putValue(cast(size_t)index); 290 } 291 } 292 serializer.arrayEnd(state); 293 } else { 294 serializer.putKey("textures"); 295 auto state = serializer.arrayBegin(); 296 serializer.elemBegin; 297 serializer.putValue(name); 298 serializer.arrayEnd(state); 299 } 300 301 serializer.putKey("blend_mode"); 302 serializer.serializeValue(blendingMode); 303 304 serializer.putKey("tint"); 305 tint.serialize(serializer); 306 307 serializer.putKey("screenTint"); 308 screenTint.serialize(serializer); 309 310 if (masks.length > 0) { 311 serializer.putKey("masks"); 312 auto state = serializer.arrayBegin(); 313 foreach(m; masks) { 314 serializer.elemBegin; 315 serializer.serializeValue(m); 316 } 317 serializer.arrayEnd(state); 318 } 319 320 321 serializer.putKey("mask_threshold"); 322 serializer.putValue(maskAlphaThreshold); 323 324 serializer.putKey("opacity"); 325 serializer.putValue(opacity); 326 327 } 328 329 override 330 SerdeException deserializeFromFghj(Fghj data) { 331 super.deserializeFromFghj(data); 332 333 334 version(InRenderless) { 335 if (inIsINPMode()) { 336 foreach(texElement; data["textures"].byElement) { 337 uint textureId; 338 texElement.deserializeValue(textureId); 339 textureIds ~= textureId; 340 } 341 } else { 342 assert(0, "Raw Inochi2D JSON not supported in renderless mode"); 343 } 344 345 // Do nothing in this instance 346 } else { 347 if (inIsINPMode()) { 348 349 foreach(texElement; data["textures"].byElement) { 350 uint textureId; 351 texElement.deserializeValue(textureId); 352 textureIds ~= textureId; 353 this.textures ~= inGetTextureFromId(textureId); 354 } 355 } else { 356 357 // TODO: Index textures by ID 358 string texName; 359 auto elements = data["textures"].byElement; 360 if (!elements.empty) { 361 if (auto exc = elements.front.deserializeValue(texName)) return exc; 362 this.textures = [new Texture(texName)]; 363 } 364 } 365 } 366 367 data["opacity"].deserializeValue(this.opacity); 368 data["mask_threshold"].deserializeValue(this.maskAlphaThreshold); 369 370 // Older models may not have tint 371 if (!data["tint"].isEmpty) deserialize(tint, data["tint"]); 372 373 // Older models may not have screen tint 374 if (!data["screenTint"].isEmpty) deserialize(screenTint, data["screenTint"]); 375 376 // Older models may not have blend mode 377 if (!data["blend_mode"].isEmpty) data["blend_mode"].deserializeValue(this.blendingMode); 378 379 if (!data["masked_by"].isEmpty) { 380 MaskingMode mode; 381 data["mask_mode"].deserializeValue(mode); 382 383 // Go every masked part 384 foreach(imask; data["masked_by"].byElement) { 385 uint uuid; 386 if (auto exc = imask.deserializeValue(uuid)) return exc; 387 this.masks ~= MaskBinding(uuid, mode, null); 388 } 389 } 390 391 if (!data["masks"].isEmpty) { 392 data["masks"].deserializeValue(this.masks); 393 } 394 395 // Update indices and vertices 396 this.updateUVs(); 397 return null; 398 } 399 400 // 401 // PARAMETER OFFSETS 402 // 403 float offsetMaskThreshold = 0; 404 float offsetOpacity = 1; 405 vec3 offsetTint = vec3(0); 406 vec3 offsetScreenTint = vec3(0); 407 408 // TODO: Cache this 409 size_t maskCount() { 410 size_t c; 411 foreach(m; masks) if (m.mode == MaskingMode.Mask) c++; 412 return c; 413 } 414 415 size_t dodgeCount() { 416 size_t c; 417 foreach(m; masks) if (m.mode == MaskingMode.DodgeMask) c++; 418 return c; 419 } 420 421 public: 422 /** 423 List of textures this part can use 424 425 TODO: use more than texture 0 426 */ 427 Texture[] textures; 428 429 /** 430 List of texture IDs 431 */ 432 int[] textureIds; 433 434 /** 435 List of masks to apply 436 */ 437 MaskBinding[] masks; 438 439 /** 440 Blending mode 441 */ 442 BlendMode blendingMode = BlendMode.Normal; 443 444 /** 445 Alpha Threshold for the masking system, the higher the more opaque pixels will be discarded in the masking process 446 */ 447 float maskAlphaThreshold = 0.5; 448 449 /** 450 Opacity of the mesh 451 */ 452 float opacity = 1; 453 454 /** 455 Multiplicative tint color 456 */ 457 vec3 tint = vec3(1, 1, 1); 458 459 /** 460 Screen tint color 461 */ 462 vec3 screenTint = vec3(0, 0, 0); 463 464 /** 465 Gets the active texture 466 */ 467 Texture activeTexture() { 468 return textures[0]; 469 } 470 471 /** 472 Constructs a new part 473 */ 474 this(MeshData data, Texture[] textures, Node parent = null) { 475 this(data, textures, inCreateUUID(), parent); 476 } 477 478 /** 479 Constructs a new part 480 */ 481 this(Node parent = null) { 482 super(parent); 483 484 version(InDoesRender) glGenBuffers(1, &uvbo); 485 } 486 487 /** 488 Constructs a new part 489 */ 490 this(MeshData data, Texture[] textures, uint uuid, Node parent = null) { 491 super(data, uuid, parent); 492 this.textures = textures; 493 494 version(InDoesRender) { 495 glGenBuffers(1, &uvbo); 496 497 mvp = partShader.getUniformLocation("mvp"); 498 gopacity = partShader.getUniformLocation("opacity"); 499 500 mmvp = partMaskShader.getUniformLocation("mvp"); 501 mthreshold = partMaskShader.getUniformLocation("threshold"); 502 } 503 504 this.updateUVs(); 505 } 506 507 override 508 bool hasParam(string key) { 509 if (super.hasParam(key)) return true; 510 511 switch(key) { 512 case "alphaThreshold": 513 case "opacity": 514 case "tint.r": 515 case "tint.g": 516 case "tint.b": 517 case "screenTint.r": 518 case "screenTint.g": 519 case "screenTint.b": 520 return true; 521 default: 522 return false; 523 } 524 } 525 526 override 527 float getDefaultValue(string key) { 528 // Skip our list of our parent already handled it 529 float def = super.getDefaultValue(key); 530 if (!isNaN(def)) return def; 531 532 switch(key) { 533 case "alphaThreshold": 534 return 0; 535 case "opacity": 536 case "tint.r": 537 case "tint.g": 538 case "tint.b": 539 return 1; 540 case "screenTint.r": 541 case "screenTint.g": 542 case "screenTint.b": 543 return 0; 544 default: return float(); 545 } 546 } 547 548 override 549 bool setValue(string key, float value) { 550 551 // Skip our list of our parent already handled it 552 if (super.setValue(key, value)) return true; 553 554 switch(key) { 555 case "alphaThreshold": 556 offsetMaskThreshold = value; 557 return true; 558 case "opacity": 559 offsetOpacity = value; 560 return true; 561 case "tint.r": 562 offsetTint.x = value; 563 return true; 564 case "tint.g": 565 offsetTint.y = value; 566 return true; 567 case "tint.b": 568 offsetTint.z = value; 569 return true; 570 case "screenTint.r": 571 offsetScreenTint.x = value; 572 return true; 573 case "screenTint.g": 574 offsetScreenTint.y = value; 575 return true; 576 case "screenTint.b": 577 offsetScreenTint.z = value; 578 return true; 579 default: return false; 580 } 581 } 582 583 bool isMaskedBy(Drawable drawable) { 584 foreach(mask; masks) { 585 if (mask.maskSrc.uuid == drawable.uuid) return true; 586 } 587 return false; 588 } 589 590 ptrdiff_t getMaskIdx(Drawable drawable) { 591 if (drawable is null) return -1; 592 foreach(i, ref mask; masks) { 593 if (mask.maskSrc.uuid == drawable.uuid) return i; 594 } 595 return -1; 596 } 597 598 ptrdiff_t getMaskIdx(uint uuid) { 599 foreach(i, ref mask; masks) { 600 if (mask.maskSrc.uuid == uuid) return i; 601 } 602 return -1; 603 } 604 605 override 606 void beginUpdate() { 607 offsetMaskThreshold = 0; 608 offsetOpacity = 1; 609 offsetTint = vec3(1, 1, 1); 610 offsetScreenTint = vec3(0, 0, 0); 611 super.beginUpdate(); 612 } 613 614 override 615 void rebuffer(ref MeshData data) { 616 super.rebuffer(data); 617 this.updateUVs(); 618 } 619 620 override 621 void draw() { 622 if (!enabled) return; 623 this.drawOne(); 624 625 foreach(child; children) { 626 child.draw(); 627 } 628 } 629 630 override 631 void drawOne() { 632 version (InDoesRender) { 633 if (!enabled) return; 634 if (!data.isReady) return; // Yeah, don't even try 635 636 size_t cMasks = maskCount; 637 638 if (masks.length > 0) { 639 import std.stdio : writeln; 640 inBeginMask(cMasks > 0); 641 642 foreach(ref mask; masks) { 643 mask.maskSrc.renderMask(mask.mode == MaskingMode.DodgeMask); 644 } 645 646 inBeginMaskContent(); 647 648 // We are the content 649 this.drawSelf(); 650 651 inEndMask(); 652 return; 653 } 654 655 this.drawSelf(); 656 } 657 super.drawOne(); 658 } 659 660 override 661 void drawOneDirect(bool forMasking) { 662 if (forMasking) this.drawSelf!true(); 663 else this.drawSelf!false(); 664 } 665 666 override 667 void finalize() { 668 super.finalize(); 669 foreach(i; 0..masks.length) { 670 if (Drawable nMask = puppet.find!Drawable(masks[i].maskSrcUUID)) { 671 masks[i].maskSrc = nMask; 672 } 673 } 674 } 675 } 676 677 /** 678 Draws a texture at the transform of the specified part 679 */ 680 void inDrawTextureAtPart(Texture texture, Part part) { 681 const float texWidthP = texture.width()/2; 682 const float texHeightP = texture.height()/2; 683 684 // Bind the vertex array 685 incDrawableBindVAO(); 686 687 partShader.use(); 688 partShader.setUniform(mvp, 689 inGetCamera().matrix * 690 mat4.translation(vec3(part.transform.matrix() * vec4(1, 1, 1, 1))) 691 ); 692 partShader.setUniform(gopacity, part.opacity); 693 partShader.setUniform(gMultColor, part.tint); 694 partShader.setUniform(gScreenColor, part.screenTint); 695 696 // Bind the texture 697 texture.bind(); 698 699 // Enable points array 700 glEnableVertexAttribArray(0); 701 glBindBuffer(GL_ARRAY_BUFFER, sVertexBuffer); 702 glBufferData(GL_ARRAY_BUFFER, 4*vec2.sizeof, [ 703 -texWidthP, -texHeightP, 704 texWidthP, -texHeightP, 705 -texWidthP, texHeightP, 706 texWidthP, texHeightP, 707 ].ptr, GL_STATIC_DRAW); 708 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 709 710 // Enable UVs array 711 glEnableVertexAttribArray(1); // uvs 712 glBindBuffer(GL_ARRAY_BUFFER, sUVBuffer); 713 glBufferData(GL_ARRAY_BUFFER, 4*vec2.sizeof, [ 714 0, 0, 715 1, 0, 716 0, 1, 717 1, 1, 718 ].ptr, GL_STATIC_DRAW); 719 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, null); 720 721 // Bind element array and draw our mesh 722 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sElementBuffer); 723 glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6*ushort.sizeof, (cast(ushort[])[ 724 0u, 1u, 2u, 725 2u, 1u, 3u 726 ]).ptr, GL_STATIC_DRAW); 727 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, null); 728 729 // Disable the vertex attribs after use 730 glDisableVertexAttribArray(0); 731 glDisableVertexAttribArray(1); 732 } 733 734 /** 735 Draws a texture at the transform of the specified part 736 */ 737 void inDrawTextureAtPosition(Texture texture, vec2 position, float opacity = 1, vec3 color = vec3(1, 1, 1), vec3 screenColor = vec3(1, 1, 1)) { 738 const float texWidthP = texture.width()/2; 739 const float texHeightP = texture.height()/2; 740 741 // Bind the vertex array 742 incDrawableBindVAO(); 743 744 partShader.use(); 745 partShader.setUniform(mvp, 746 inGetCamera().matrix * 747 mat4.scaling(1, 1, 1) * 748 mat4.translation(vec3(position, 0)) 749 ); 750 partShader.setUniform(gopacity, opacity); 751 partShader.setUniform(gMultColor, color); 752 partShader.setUniform(gScreenColor, screenColor); 753 754 // Bind the texture 755 texture.bind(); 756 757 // Enable points array 758 glEnableVertexAttribArray(0); 759 glBindBuffer(GL_ARRAY_BUFFER, sVertexBuffer); 760 glBufferData(GL_ARRAY_BUFFER, 4*vec2.sizeof, [ 761 -texWidthP, -texHeightP, 762 texWidthP, -texHeightP, 763 -texWidthP, texHeightP, 764 texWidthP, texHeightP, 765 ].ptr, GL_STATIC_DRAW); 766 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 767 768 // Enable UVs array 769 glEnableVertexAttribArray(1); // uvs 770 glBindBuffer(GL_ARRAY_BUFFER, sUVBuffer); 771 glBufferData(GL_ARRAY_BUFFER, 4*vec2.sizeof, (cast(float[])[ 772 0, 0, 773 1, 0, 774 0, 1, 775 1, 1, 776 ]).ptr, GL_STATIC_DRAW); 777 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, null); 778 779 // Bind element array and draw our mesh 780 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sElementBuffer); 781 glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6*ushort.sizeof, (cast(ushort[])[ 782 0u, 1u, 2u, 783 2u, 1u, 3u 784 ]).ptr, GL_STATIC_DRAW); 785 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, null); 786 787 // Disable the vertex attribs after use 788 glDisableVertexAttribArray(0); 789 glDisableVertexAttribArray(1); 790 }