- transition smoothly between idle and run animations - use quaternions (versors) instead of yaw/pitch/roll - hide mouse and enable free-look mode by default on startup - rotate model 180 degrees in Blender so it faces away from the camera by default - add some makefile commands
507 lines
15 KiB
C
507 lines
15 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include "SDL2/SDL.h"
|
|
#include "GL/glew.h"
|
|
#include "SDL2/SDL_opengl.h"
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
#include "stb/stb_image.h"
|
|
#include "cglm/cglm.h"
|
|
#include "cgltf/cgltf.h"
|
|
#include "render.h"
|
|
#include "camera.h"
|
|
#include "game.h"
|
|
#include "shape.h"
|
|
#include "model.h"
|
|
|
|
static void LoadTextureArray(GLuint texture, const char* filePath, int nCols, int nRows)
|
|
{
|
|
const int nMipmaps = 1;
|
|
int nTiles = nCols * nRows;
|
|
int tWidth, tHeight, tChannels;
|
|
|
|
unsigned char* textureBytes = stbi_load(filePath, &tWidth, &tHeight, &tChannels, 0);
|
|
|
|
tWidth /= nCols;
|
|
tHeight /= nRows;
|
|
|
|
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
|
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, tWidth, tHeight, nTiles, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
|
|
|
int tBytes = 4 * tWidth * tHeight;
|
|
unsigned char* buffer = malloc(tBytes * sizeof(unsigned char));
|
|
|
|
for (int z = 0; z < nTiles; z++)
|
|
{
|
|
for (int b = 0; b < tBytes; b++)
|
|
{
|
|
int bWidth = 4 * tWidth; // width of each tile in bytes
|
|
int i = ((z / nCols) * nCols * bWidth * tHeight) // size (in bytes) of all tiles above
|
|
+ ((b / bWidth) * bWidth * nCols) // size of pixel rows above, within the current tile row
|
|
+ ((z % nCols) * bWidth) // width of tiles to the left
|
|
+ (b % bWidth); // width from the left side of the current tile
|
|
|
|
buffer[b] = textureBytes[i];
|
|
}
|
|
|
|
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, z, tWidth, tHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
|
|
}
|
|
|
|
free(buffer);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
|
|
|
|
stbi_image_free(textureBytes);
|
|
}
|
|
|
|
static void LoadTexture(GameState* gs)
|
|
{
|
|
glGenTextures(1, &gs->textureId);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glUniform1i(glGetUniformLocation(gs->shaderProgramId, "textureSampler"), 0);
|
|
LoadTextureArray(gs->textureId, "res/tex/texture.png", 8, 4);
|
|
printf("Loaded the texture array.\n");
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
}
|
|
|
|
static char **ReadFileLines(const char *path, int *n)
|
|
{
|
|
const int MAX_LINES = 128;
|
|
const int MAX_LINE_LENGTH = 256;
|
|
|
|
FILE *file = fopen(path, "r");
|
|
char line[MAX_LINE_LENGTH];
|
|
char **lines = calloc(MAX_LINES, sizeof(char*));
|
|
*n = 0;
|
|
|
|
if (lines == NULL) return NULL;
|
|
|
|
while (fgets(&(line[0]), MAX_LINE_LENGTH, file) != NULL && *n < MAX_LINES)
|
|
{
|
|
GLchar *l = strdup(&(line[0]));
|
|
if (l == NULL) return NULL;
|
|
lines[*n] = l;
|
|
(*n)++;
|
|
}
|
|
|
|
fclose(file);
|
|
int s = sizeof(char*) * (*n);
|
|
lines = realloc(lines, s);
|
|
|
|
return lines;
|
|
}
|
|
|
|
static void FreeLines(char **lines, int n)
|
|
{
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
free(lines[i]);
|
|
}
|
|
|
|
free(lines);
|
|
}
|
|
|
|
static int CompileShader(GLchar *sourcePath, GLenum shaderType, char ***outCode, int *outNumLines)
|
|
{
|
|
GLint success;
|
|
*outCode = ReadFileLines(sourcePath, outNumLines);
|
|
GLuint shaderId = glCreateShader(shaderType);
|
|
glShaderSource(shaderId, *outNumLines, (void*)(*outCode), NULL);
|
|
glCompileShader(shaderId);
|
|
glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
|
|
|
|
if (!success)
|
|
{
|
|
GLchar log[512];
|
|
glGetShaderInfoLog(shaderId, 512, NULL, log);
|
|
printf("%s\n", log);
|
|
return -1;
|
|
}
|
|
|
|
return shaderId;
|
|
}
|
|
|
|
static int LoadShaders(const char *vertShaderFilePath, const char *fragShaderFilePath)
|
|
{
|
|
char **vertCode = NULL;
|
|
int vertNumLines;
|
|
int vertShaderId = CompileShader(vertShaderFilePath, GL_VERTEX_SHADER, &vertCode, &vertNumLines);
|
|
if (vertShaderId < 0) return vertShaderId;
|
|
|
|
char **fragCode = NULL;
|
|
int fragNumLines;
|
|
int fragShaderId = CompileShader(fragShaderFilePath, GL_FRAGMENT_SHADER, &fragCode, &fragNumLines);
|
|
if (fragShaderId < 0 ) return fragShaderId;
|
|
|
|
GLint success;
|
|
GLuint shaderProgramId = glCreateProgram();
|
|
glAttachShader(shaderProgramId, vertShaderId);
|
|
glAttachShader(shaderProgramId, fragShaderId);
|
|
glLinkProgram(shaderProgramId);
|
|
glGetProgramiv(shaderProgramId, GL_LINK_STATUS, &success);
|
|
|
|
if (!success)
|
|
{
|
|
GLchar log[512];
|
|
glGetProgramInfoLog(shaderProgramId, 512, NULL, log);
|
|
printf("%s", log);
|
|
return -1;
|
|
}
|
|
|
|
glDeleteShader(vertShaderId);
|
|
glDeleteShader(fragShaderId);
|
|
FreeLines(vertCode, vertNumLines);
|
|
FreeLines(fragCode, fragNumLines);
|
|
|
|
return shaderProgramId;
|
|
}
|
|
|
|
static void InitShapeBuffers(Shape *shape)
|
|
{
|
|
glBindVertexArray(shape->VAO);
|
|
|
|
// vertex buffer: contains vertex and texture coords
|
|
glBindBuffer(GL_ARRAY_BUFFER, shape->VBO);
|
|
glBufferData(GL_ARRAY_BUFFER, shape->numVertices * sizeof(GLfloat), shape->vertices, GL_STATIC_DRAW);
|
|
GLsizei stride = 5 * sizeof(GLfloat);
|
|
|
|
// vertex coordinates
|
|
glEnableVertexAttribArray(0);
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)0);
|
|
|
|
// texture coordinates
|
|
glEnableVertexAttribArray(1);
|
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(3 * sizeof(GLfloat)));
|
|
|
|
// instance buffer: contains per-instance texture index and model transformation matrix
|
|
// therefore each instance of the same shape, in the same draw call, can have a different texture and transformation
|
|
glBindBuffer(GL_ARRAY_BUFFER, shape->IBO);
|
|
stride = SHAPE_INSTANCE_SIZE;
|
|
|
|
// texture index: basically another tex coord in the third dimension because a texture array is used
|
|
glEnableVertexAttribArray(2);
|
|
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, stride, (GLvoid*)0);
|
|
glVertexAttribDivisor(2, 1);
|
|
|
|
// model matrix: contains 16 floats, each vert attrib has space for 4 floats, therefore it spans 4 attrib locations
|
|
glEnableVertexAttribArray(3);
|
|
glEnableVertexAttribArray(4);
|
|
glEnableVertexAttribArray(5);
|
|
glEnableVertexAttribArray(6);
|
|
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(1 * sizeof(GLfloat)));
|
|
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(5 * sizeof(GLfloat)));
|
|
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(9 * sizeof(GLfloat)));
|
|
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(13 * sizeof(GLfloat)));
|
|
glVertexAttribDivisor(3, 1);
|
|
glVertexAttribDivisor(4, 1);
|
|
glVertexAttribDivisor(5, 1);
|
|
glVertexAttribDivisor(6, 1);
|
|
|
|
// index buffer: contains vertex indices, reducing the data to be buffered on the GPU
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, shape->EBO);
|
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, shape->numIndices * sizeof(GLushort), shape->indices, GL_STATIC_DRAW);
|
|
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
bool Render_Init(GameState *gs)
|
|
{
|
|
SDL_Init(SDL_INIT_VIDEO);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
|
|
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
|
|
gs->window = SDL_CreateWindow("Sandbox", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);;
|
|
gs->glContext = SDL_GL_CreateContext(gs->window);
|
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
|
printf("Created OpenGL window.\n");
|
|
|
|
if (SDL_GL_SetSwapInterval(1) == 0) printf("Enabled VSync.\n");
|
|
else printf("Failed to enable VSync. SDL Error: %s\n", SDL_GetError());
|
|
|
|
glewInit();
|
|
gs->shaderProgramId = LoadShaders("./res/glsl/vertex.glsl", "./res/glsl/fragment.glsl");
|
|
gs->modelShaderProgramId = LoadShaders("./res/glsl/model_vertex.glsl", "./res/glsl/fragment.glsl");
|
|
|
|
if (gs->shaderProgramId < 0 || gs->modelShaderProgramId < 0)
|
|
{
|
|
printf("Failed to load shaders.\n");
|
|
return false;
|
|
}
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
glEnable(GL_CULL_FACE);
|
|
glEnable(GL_TEXTURE_2D);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
printf("Initialized OpenGL.\n");
|
|
|
|
gs->numShapes = 2;
|
|
|
|
Shape *shape = Shape_MakePyramid(5);
|
|
gs->shapes[0] = shape;
|
|
glGenVertexArrays(1, &shape->VAO);
|
|
glGenBuffers(1, &shape->VBO);
|
|
glGenBuffers(1, &shape->IBO);
|
|
glGenBuffers(1, &shape->EBO);
|
|
printf("Created shape 0.\n");
|
|
|
|
shape = Shape_MakePlane();
|
|
gs->shapes[1] = shape;
|
|
glGenVertexArrays(1, &shape->VAO);
|
|
glGenBuffers(1, &shape->VBO);
|
|
glGenBuffers(1, &shape->IBO);
|
|
glGenBuffers(1, &shape->EBO);
|
|
printf("Created shape 1.\n");
|
|
|
|
LoadTexture(gs);
|
|
printf("Loaded textures.\n");
|
|
Camera_Init(&gs->camera);
|
|
|
|
InitShapeBuffers(gs->shapes[0]);
|
|
InitShapeBuffers(gs->shapes[1]);
|
|
printf("Initialized buffers.\n");
|
|
|
|
gs->testModel = Model_LoadGltf("res/model/human.glb");
|
|
|
|
return true;
|
|
}
|
|
|
|
void Render_Destroy(GameState *gs)
|
|
{
|
|
Model_Free(gs->testModel);
|
|
SDL_GL_DeleteContext(gs->glContext);
|
|
SDL_DestroyWindow(gs->window);
|
|
}
|
|
|
|
// Sets the GL viewport while maintaining aspect ratio.
|
|
// Calculates the projection and view matrices.
|
|
static void SetViewport(GameState *gs)
|
|
{
|
|
const float ratio = 0.5625f; // 1080/1920
|
|
int wWidth, wHeight, offsetX, offsetY;
|
|
SDL_GL_GetDrawableSize(gs->window, &wWidth, &wHeight);
|
|
|
|
if ((float)wHeight / (float)wWidth > ratio)
|
|
{
|
|
int temp = (int)floorf(ratio * (float)wWidth);
|
|
offsetX = 0;
|
|
offsetY = (wHeight - temp) / 2;
|
|
wHeight = temp;
|
|
}
|
|
else
|
|
{
|
|
int temp = (int)floorf((float)wHeight / ratio);
|
|
offsetX = (wWidth - temp) / 2;
|
|
offsetY = 0;
|
|
wWidth = temp;
|
|
}
|
|
|
|
glViewport(offsetX, offsetY, wWidth, wHeight);
|
|
|
|
glm_mat4_identity(gs->projMatrix);
|
|
glm_perspective(45.0f, (GLfloat)wWidth / (GLfloat)wHeight, 0.1f, 2000.0f, gs->projMatrix);
|
|
|
|
glm_mat4_identity(gs->viewMatrix);
|
|
Camera_GetViewMatrix(&gs->camera, gs->viewMatrix);
|
|
}
|
|
|
|
static void Transform(ShapeInstance *instance, mat4 *matrix)
|
|
{
|
|
mat4 tempMat;
|
|
glm_mat4_identity(tempMat);
|
|
|
|
glm_translate(tempMat, instance->position);
|
|
glm_quat_rotate(tempMat, instance->rotation, tempMat);
|
|
|
|
vec3 scale;
|
|
scale[0] = instance->scale;
|
|
scale[1] = instance->scale;
|
|
scale[2] = instance->scale;
|
|
glm_scale(tempMat, scale);
|
|
|
|
memcpy(matrix, tempMat, sizeof(mat4));
|
|
}
|
|
|
|
static void ApplyAnim(cgltf_animation *anim, float t, float blend)
|
|
{
|
|
for (int i = 0; i < anim->channels_count; i++)
|
|
{
|
|
cgltf_animation_channel* channel = &anim->channels[i];
|
|
cgltf_animation_sampler* sampler = channel->sampler;
|
|
cgltf_node* node = channel->target_node;
|
|
|
|
// find keyframe interval [k, k+1]
|
|
size_t k = 0;
|
|
float alpha = 0.0f;
|
|
size_t num_keyframes = sampler->input->count;
|
|
|
|
// binary search may be faster
|
|
for (size_t j = 0; j < num_keyframes - 1; ++j)
|
|
{
|
|
float t0, t1;
|
|
cgltf_accessor_read_float(sampler->input, j, &t0, 1);
|
|
cgltf_accessor_read_float(sampler->input, j + 1, &t1, 1);
|
|
|
|
if (t >= t0 && t <= t1)
|
|
{
|
|
k = j;
|
|
float range = t1 - t0;
|
|
alpha = (range > 0.0f) ? (t - t0) / range : 0.0f;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// interpolate
|
|
switch (channel->target_path)
|
|
{
|
|
case cgltf_animation_path_type_translation:
|
|
{
|
|
vec3 p0, p1, result;
|
|
cgltf_accessor_read_float(sampler->output, k, p0, 3);
|
|
cgltf_accessor_read_float(sampler->output, k + 1, p1, 3);
|
|
glm_vec3_lerp(p0, p1, alpha, result);
|
|
// glm_vec3_copy is not used here due to potential struct alignment issues
|
|
memcpy(p0, node->translation, sizeof(vec3));
|
|
glm_vec3_lerp(p0, result, blend, result);
|
|
memcpy(node->translation, result, sizeof(vec3));
|
|
node->has_translation = true;
|
|
}
|
|
break;
|
|
case cgltf_animation_path_type_rotation:
|
|
{
|
|
versor q0, q1, result;
|
|
cgltf_accessor_read_float(sampler->output, k, q0, 4);
|
|
cgltf_accessor_read_float(sampler->output, k + 1, q1, 4);
|
|
// slerp (not lerp) for rotation
|
|
glm_quat_slerp(q0, q1, alpha, result);
|
|
memcpy(q0, node->rotation, sizeof(versor));
|
|
glm_quat_slerp(q0, result, blend, result);
|
|
memcpy(node->rotation, result, sizeof(versor));
|
|
|
|
node->has_rotation = true;
|
|
}
|
|
break;
|
|
case cgltf_animation_path_type_scale:
|
|
{
|
|
vec3 s0, s1, result;
|
|
cgltf_accessor_read_float(sampler->output, k, s0, 3);
|
|
cgltf_accessor_read_float(sampler->output, k + 1, s1, 3);
|
|
glm_vec3_lerp(s0, s1, alpha, result);
|
|
memcpy(s0, node->scale, sizeof(vec3));
|
|
glm_vec3_lerp(s0, result, blend, result);
|
|
memcpy(node->scale, result, sizeof(vec3));
|
|
node->has_scale = true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void DrawModel(GameState *gs)
|
|
{
|
|
Model *model = &gs->testModel;
|
|
ShapeInstance *instance = &model->instance;
|
|
cgltf_data *data = model->data;
|
|
cgltf_skin *skin = model->skin;
|
|
mat4 modelMatrix;
|
|
glm_mat4_identity(modelMatrix);
|
|
Transform(instance, &modelMatrix);
|
|
|
|
uint64_t ticks = SDL_GetTicks64();
|
|
float t = ticks / 1000.0f;
|
|
while (t > 1.0f) t -= 1.0f;
|
|
|
|
// apply local transformations for each bone for the current animation frame
|
|
ApplyAnim(model->idle, t, 1.0f);
|
|
ApplyAnim(model->run, t, gs->animBlend);
|
|
|
|
if (!gs->input.freeLook)
|
|
{
|
|
// make head look where camera is looking
|
|
cgltf_node *head = model->head;
|
|
vec3 modelFront, camFront;
|
|
glm_quat_rotatev(instance->rotation, GLM_FORWARD, modelFront);
|
|
glm_vec3_copy(gs->camera.front, camFront);
|
|
modelFront[1] = 0.0f;
|
|
camFront[1] = 0.0f;
|
|
versor desiredRotation, temp;
|
|
glm_quat_from_vecs(modelFront, camFront, desiredRotation);
|
|
memcpy(temp, head->rotation, sizeof(versor));
|
|
glm_quat_mul(temp, desiredRotation, temp);
|
|
memcpy(head->rotation, temp, sizeof(versor));
|
|
}
|
|
|
|
// recalculate the joint matrices
|
|
for (int i = 0; i < skin->joints_count; i++)
|
|
{
|
|
cgltf_node *joint = skin->joints[i];
|
|
cgltf_node_transform_world(joint, (float*)model->jointMatrices[i]);
|
|
mat4 inverseBind;
|
|
cgltf_accessor_read_float(skin->inverse_bind_matrices, i, (float*)inverseBind, 16);
|
|
glm_mat4_mul(model->jointMatrices[i], inverseBind, model->jointMatrices[i]);
|
|
}
|
|
|
|
int shaderId = gs->modelShaderProgramId;
|
|
glUseProgram(shaderId);
|
|
glUniformMatrix4fv(glGetUniformLocation(shaderId, "theModelMatrix"), 1, GL_FALSE, (void*)(modelMatrix));
|
|
glUniformMatrix4fv(glGetUniformLocation(shaderId, "theViewMatrix"), 1, GL_FALSE, (void*)(gs->viewMatrix));
|
|
glUniformMatrix4fv(glGetUniformLocation(shaderId, "theProjMatrix"), 1, GL_FALSE, (void*)(gs->projMatrix));
|
|
glUniformMatrix4fv(glGetUniformLocation(shaderId, "theJointMatrices"), 64, GL_FALSE, (void*)(model->jointMatrices));
|
|
glUniform1f(glGetUniformLocation(shaderId, "texIndex"), 6.0f);
|
|
glBindTexture(GL_TEXTURE_2D, gs->textureId);
|
|
|
|
glBindVertexArray(model->vao);
|
|
glDrawElements(GL_TRIANGLES, model->indexCount, GL_UNSIGNED_SHORT, 0);
|
|
}
|
|
|
|
void Render_Draw(GameState *gs)
|
|
{
|
|
glClearColor(0.4f, 0.6f, 0.8f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
glLoadIdentity();
|
|
SetViewport(gs);
|
|
|
|
glUseProgram(gs->shaderProgramId);
|
|
GLint projLoc = glGetUniformLocation(gs->shaderProgramId, "theProjMatrix");
|
|
glUniformMatrix4fv(projLoc, 1, GL_FALSE, (void*)(gs->projMatrix));
|
|
GLint viewLoc = glGetUniformLocation(gs->shaderProgramId, "theViewMatrix");
|
|
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, (void*)(gs->viewMatrix));
|
|
glBindTexture(GL_TEXTURE_2D, gs->textureId);
|
|
vec3 scale = { 0, 0, 0 };
|
|
|
|
for (int i = 0; i < gs->numShapes; i++)
|
|
{
|
|
Shape *shape = gs->shapes[i];
|
|
glBindVertexArray(shape->VAO);
|
|
|
|
// apply transformations to each instance
|
|
for (int j = 0; j < shape->numInstances; j++)
|
|
{
|
|
ShapeInstance* instance = shape->instances + j;
|
|
mat4* matrix = (void*)(shape->instanceData + (j * 17) + 1);
|
|
Transform(instance, matrix);
|
|
}
|
|
|
|
// re-buffer the instance data because transformations may have changed
|
|
glBindBuffer(GL_ARRAY_BUFFER, shape->IBO);
|
|
glBufferData(GL_ARRAY_BUFFER, shape->numInstances * SHAPE_INSTANCE_SIZE, shape->instanceData, GL_DYNAMIC_DRAW);
|
|
|
|
glDrawElementsInstanced(GL_TRIANGLES, shape->numIndices, GL_UNSIGNED_SHORT, 0, shape->numInstances);
|
|
}
|
|
|
|
DrawModel(gs);
|
|
|
|
SDL_GL_SwapWindow(gs->window);
|
|
}
|