-
-
Save OllieReynolds/71bc384009be2e2e7bb7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define GLFW_DLL | |
#include <GL/glew.h> | |
#include <GLFW/glfw3.h> | |
#include <glm\mat4x4.hpp> | |
#include <glm/gtc/type_ptr.hpp> | |
#include <glm\gtc\quaternion.hpp> | |
#include <glm\gtc\matrix_transform.hpp> | |
#include <SOIL.h> | |
#include <string> | |
#include "Shader.hpp" | |
#include "Cube.hpp" | |
#include "lighting.hpp" | |
#include "camera.hpp" | |
#include "material.hpp" | |
#include "utility.hpp" | |
#include "model.hpp" | |
#pragma region "Constants" | |
const char* TITLE = "Predator-Prey Simulation"; | |
const float WIDTH = 1920.f; | |
const float HEIGHT = 1080.f; | |
const float FIELD_OF_VIEW = 90.f; | |
const float NEAR_PLANE = 0.1f; | |
const float FAR_PLANE = 100.f; | |
const glm::vec3 START_CAMERA_POSITION = glm::vec3(0.f, 2.f, 4.f); | |
const glm::vec3 START_CAMERA_FRONT = glm::vec3(0.f, 0.f, -1.f); | |
const glm::vec3 START_CAMERA_UP = glm::vec3(0.f, 1.f, 0.f); | |
const char* DEFAULT_VERTEX_SHADER_FILE = "vert.glsl"; | |
const char* DEFAULT_FRAGMENT_SHADER_FILE = "frag.glsl"; | |
const char* LIGHT_OBJECT_FRAGMENT_SHADER_FILE = "light.glsl"; | |
const bool USE_POINT_LIGHTING = true; | |
const bool USE_DIRECTIONAL_LIGHTING = true; | |
const bool USE_SPOT_LIGHTING = true; | |
const int NUM_POINT_LIGHTS = 3; | |
#pragma endregion | |
Model model; | |
Material material; | |
PointLight pointLight[NUM_POINT_LIGHTS]; | |
DirectionalLight directionalLight; | |
SpotLight spotLight; | |
Camera camera; | |
Orientation orientation; | |
FrameTiming frameTiming; | |
Input input; | |
Shader shader; | |
Shader lightShader; | |
Cube cube; | |
Cube cubeLamp; | |
GLFWwindow* window; | |
glm::mat4 projectionMatrix; | |
glm::vec3 cubePositions[] = { | |
glm::vec3(0.0f, 0.0f, 0.0f), | |
glm::vec3(2.0f, 5.0f, -4.0f), | |
glm::vec3(-1.5f, -2.2f, -2.5f), | |
glm::vec3(-3.8f, -2.0f, -6.3f), | |
glm::vec3(2.4f, -0.4f, -3.5f), | |
glm::vec3(-1.7f, 3.0f, -7.5f), | |
glm::vec3(1.3f, -2.0f, -2.5f), | |
glm::vec3(1.5f, 2.0f, -2.5f), | |
glm::vec3(1.5f, 0.2f, -1.5f), | |
glm::vec3(-1.3f, 1.0f, -1.5f) | |
}; | |
GLuint amount = 104907; | |
glm::mat4* modelMatrices; | |
float* instanceColours; | |
void instancedRender() { | |
lightShader.use(); | |
glUniformMatrix4fv(glGetUniformLocation(lightShader.program, "projection"), 1, GL_FALSE, value_ptr(projectionMatrix)); | |
glUniformMatrix4fv(glGetUniformLocation(lightShader.program, "view"), 1, GL_FALSE, value_ptr(camera.viewMatrix)); | |
glUniform3fv(glGetUniformLocation(lightShader.program, "pointLight.ambient"), 1, value_ptr(glm::vec3(1.f))); | |
glUniform3fv(glGetUniformLocation(lightShader.program, "pointLight.diffuse"), 1, value_ptr(glm::vec3(1.f))); | |
glUniform3fv(glGetUniformLocation(lightShader.program, "pointLight.specular"), 1, value_ptr(glm::vec3(1.f))); | |
glBindVertexArray(cube.vao); | |
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, amount); | |
glBindVertexArray(0); | |
} | |
void initInstancedRender() { | |
lightShader = Shader(DEFAULT_VERTEX_SHADER_FILE, LIGHT_OBJECT_FRAGMENT_SHADER_FILE); | |
camera.position = START_CAMERA_POSITION; | |
camera.front = START_CAMERA_FRONT; | |
camera.up = START_CAMERA_UP; | |
camera.speed = 50.f; | |
camera.viewMatrix = lookAt(camera.position, camera.position + camera.front, camera.up); | |
projectionMatrix = glm::infinitePerspective(glm::radians(FIELD_OF_VIEW), WIDTH / HEIGHT, 0.1f); | |
modelMatrices = new glm::mat4[amount]; | |
int index = 0; | |
for (GLint x = 0; x < 187; x++) { | |
for (GLint z = 0; z < 561; z++) { | |
glm::mat4 model; | |
model = glm::scale(model, glm::vec3(3.F, 1.F, 1.F)); | |
model = glm::translate(model, glm::vec3(-x * 1.9F, 0.f, -z * 1.9F)); | |
modelMatrices[index++] = model; | |
} | |
} | |
instanceColours = new float[amount]; | |
std::fstream in; | |
in.open("data5.txt", std::fstream::in); | |
std::string line; | |
std::vector<float> v; | |
std::getline(in, line); | |
float value; | |
std::istringstream iss(line); | |
std::copy(std::istream_iterator<float>(iss), | |
std::istream_iterator<float>(), | |
std::back_inserter(v)); | |
in.close(); | |
int i1 = 0; | |
for (float f : v) { | |
instanceColours[i1] = v.at(i1); | |
i1++; | |
} | |
cube.init(); | |
orientation.pitch = -25.f; | |
orientation.yaw = -90.f; | |
orientation.roll = 0.f; | |
input.previousMousePosX = WIDTH / 2.f; | |
input.previousMousePosY = HEIGHT / 2.f; | |
input.mouseInitiallyMoved = true; | |
input.mouseSensitivity = 0.2f; | |
GLuint buffer; | |
GLuint buffer2; | |
glBindVertexArray(cube.vao); | |
glGenBuffers(1, &buffer); | |
glBindBuffer(GL_ARRAY_BUFFER, buffer); | |
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW); | |
// Set attribute pointers for matrix (4 times vec4) | |
glEnableVertexAttribArray(3); | |
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)0); | |
glEnableVertexAttribArray(4); | |
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)(sizeof(glm::vec4))); | |
glEnableVertexAttribArray(5); | |
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)(2 * sizeof(glm::vec4))); | |
glEnableVertexAttribArray(6); | |
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (GLvoid*)(3 * sizeof(glm::vec4))); | |
glVertexAttribDivisor(3, 1); | |
glVertexAttribDivisor(4, 1); | |
glVertexAttribDivisor(5, 1); | |
glVertexAttribDivisor(6, 1); | |
glGenBuffers(1, &buffer2); | |
glBindBuffer(GL_ARRAY_BUFFER, buffer2); | |
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(float), &instanceColours[0], GL_STATIC_DRAW); | |
glEnableVertexAttribArray(7); | |
glVertexAttribPointer(7, 1, GL_FLOAT, GL_FALSE, sizeof(float), (GLvoid*)0); | |
glVertexAttribDivisor(7, 1); | |
glBindVertexArray(0); | |
} | |
void initSimulation() | |
{ | |
shader = Shader(DEFAULT_VERTEX_SHADER_FILE, DEFAULT_FRAGMENT_SHADER_FILE); | |
lightShader = Shader(DEFAULT_VERTEX_SHADER_FILE, LIGHT_OBJECT_FRAGMENT_SHADER_FILE); | |
camera.position = START_CAMERA_POSITION; | |
camera.front = START_CAMERA_FRONT; | |
camera.up = START_CAMERA_UP; | |
camera.speed = 5.f; | |
camera.viewMatrix = lookAt(camera.position, camera.position + camera.front, camera.up); | |
projectionMatrix = glm::perspective(glm::radians(FIELD_OF_VIEW), WIDTH / HEIGHT, NEAR_PLANE, FAR_PLANE); | |
//RED | |
pointLight[0].ambient = glm::vec3(0.05f, 0.f, 0.0f); | |
pointLight[0].diffuse = glm::vec3(0.7f, 0.f, 0.0f); | |
pointLight[0].specular = glm::vec3(1.f, 0.f, 0.f); | |
pointLight[0].attenuation.constant = 1.f; | |
pointLight[0].attenuation.linear = 0.14f; | |
pointLight[0].attenuation.quadratic = 0.07f; | |
pointLight[0].position = glm::vec3(0.f); | |
//GREEN | |
pointLight[1].ambient = glm::vec3(0.0f, 0.05f, 0.0f); | |
pointLight[1].diffuse = glm::vec3(0.0f, 0.7f, 0.0f); | |
pointLight[1].specular = glm::vec3(0.f, 1.f, 0.f); | |
pointLight[1].attenuation.constant = 1.f; | |
pointLight[1].attenuation.linear = 0.14f; | |
pointLight[1].attenuation.quadratic = 0.07f; | |
pointLight[1].position = glm::vec3(1.f, 0.f, 0.f); | |
//BLUE | |
pointLight[2].ambient = glm::vec3(0.0f, 0.f, 0.05f); | |
pointLight[2].diffuse = glm::vec3(0.0f, 0.f, 0.9f); | |
pointLight[2].specular = glm::vec3(0.f, 0.f, 1.f); | |
pointLight[2].attenuation.constant = 1.f; | |
pointLight[2].attenuation.linear = 0.14f; | |
pointLight[2].attenuation.quadratic = 0.07f; | |
pointLight[2].position = glm::vec3(2.f, 0.f, 0.f); | |
directionalLight.ambient = glm::vec3(0.1f, 0.1f, 0.1f); | |
directionalLight.diffuse = glm::vec3(0.3f, 0.3f, 0.3f); | |
directionalLight.specular = glm::vec3(1.f, 1.f, 1.f); | |
directionalLight.direction = glm::vec3(0.f, 1.f, 0.f); | |
spotLight.ambient = glm::vec3(0.0f, 0.f, 1.f); | |
spotLight.diffuse = glm::vec3(0.0f, 0.f, 1.f); | |
spotLight.specular = glm::vec3(0.f, 0.f, 1.f); | |
spotLight.attenuation.constant = 1.f; | |
spotLight.attenuation.linear = 0.045f; | |
spotLight.attenuation.quadratic = 0.0075f; | |
spotLight.position = camera.position; | |
spotLight.direction = camera.front; | |
spotLight.innerCutoffAngle = 6.f; | |
spotLight.outerCutoffAngle = 20.f; | |
material.shininess = 32.f; | |
glGenTextures(1, &material.diffuseMap); | |
int width; | |
int height; | |
unsigned char* img; | |
img = SOIL_load_image("box_diffuse.png", &width, &height, nullptr, SOIL_LOAD_RGB); | |
glBindTexture(GL_TEXTURE_2D, material.diffuseMap); | |
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, img); | |
glGenerateMipmap(GL_TEXTURE_2D); | |
SOIL_free_image_data(img); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_NEAREST); | |
glGenTextures(1, &material.specularMap); | |
img = SOIL_load_image("box_specular.png", &width, &height, nullptr, SOIL_LOAD_RGB); | |
glBindTexture(GL_TEXTURE_2D, material.specularMap); | |
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, img); | |
glGenerateMipmap(GL_TEXTURE_2D); | |
SOIL_free_image_data(img); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_NEAREST); | |
cubeLamp = Cube(); | |
cubeLamp.init(); | |
cube = Cube(); | |
cube.init(); | |
model = Model("Metroid/metroid.DAE"); | |
orientation.pitch = -25.f; | |
orientation.yaw = -90.f; | |
orientation.roll = 0.f; | |
input.previousMousePosX = WIDTH / 2.f; | |
input.previousMousePosY = HEIGHT / 2.f; | |
input.mouseInitiallyMoved = true; | |
input.mouseSensitivity = 0.2f; | |
} | |
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); | |
void mouse_callback(GLFWwindow* window, double xpos, double ypos); | |
void moveCamera() { | |
auto movementFactor = camera.speed * frameTiming.deltaTime; | |
if (input.keyPressedStates[GLFW_KEY_W]) | |
camera.position += camera.front * movementFactor; | |
if (input.keyPressedStates[GLFW_KEY_S]) | |
camera.position -= camera.front * movementFactor; | |
if (input.keyPressedStates[GLFW_KEY_A]) | |
camera.position -= normalize(cross(camera.front, camera.up)) * movementFactor; | |
if (input.keyPressedStates[GLFW_KEY_D]) | |
camera.position += normalize(cross(camera.front, camera.up)) * movementFactor; | |
} | |
void renderObject() | |
{ | |
shader.use(); | |
glUniform3fv(glGetUniformLocation(shader.program, "cameraPosition"), 1, value_ptr(camera.position)); | |
glUniform1i(glGetUniformLocation(shader.program, "material.diffuse"), 0); | |
glUniform1i(glGetUniformLocation(shader.program, "material.specular"), 1); | |
glUniform1f(glGetUniformLocation(shader.program, "material.shininess"), material.shininess); | |
for (auto i = 0; i < NUM_POINT_LIGHTS; i++) | |
{ | |
auto index = std::to_string(i); | |
//pointLight[i].position.x = sin(glfwGetTime() + i * 300); | |
//pointLight[i].position.z = -2.f + cos(glfwGetTime() + i * 300); | |
//pointLight[i].position.y = 1.f; | |
glUniform3fv(glGetUniformLocation(shader.program, ("pointLight[" + index + "].ambient").c_str()), 1, value_ptr(pointLight[i].ambient)); | |
glUniform3fv(glGetUniformLocation(shader.program, ("pointLight[" + index + "].diffuse").c_str()), 1, value_ptr(pointLight[i].diffuse)); | |
glUniform3fv(glGetUniformLocation(shader.program, ("pointLight[" + index + "].specular").c_str()), 1, value_ptr(pointLight[i].specular)); | |
glUniform3fv(glGetUniformLocation(shader.program, ("pointLight[" + index + "].position").c_str()), 1, value_ptr(pointLight[i].position)); | |
glUniform1f(glGetUniformLocation(shader.program, ("pointLight[" + index + "].attenuation.constant").c_str()), pointLight[i].attenuation.constant); | |
glUniform1f(glGetUniformLocation(shader.program, ("pointLight[" + index + "].attenuation.linear").c_str()), pointLight[i].attenuation.linear); | |
glUniform1f(glGetUniformLocation(shader.program, ("pointLight[" + index + "].attenuation.quadratic").c_str()), pointLight[i].attenuation.quadratic); | |
} | |
glUniform3fv(glGetUniformLocation(shader.program, "directionalLight.ambient"), 1, value_ptr(directionalLight.ambient)); | |
glUniform3fv(glGetUniformLocation(shader.program, "directionalLight.diffuse"), 1, value_ptr(directionalLight.diffuse)); | |
glUniform3fv(glGetUniformLocation(shader.program, "directionalLight.specular"), 1, value_ptr(directionalLight.specular)); | |
glUniform3fv(glGetUniformLocation(shader.program, "directionalLight.direction"), 1, value_ptr(directionalLight.direction)); | |
spotLight.position = camera.position; | |
spotLight.direction = camera.front; | |
glUniform3fv(glGetUniformLocation(shader.program, "spotLight.ambient"), 1, value_ptr(spotLight.ambient)); | |
glUniform3fv(glGetUniformLocation(shader.program, "spotLight.diffuse"), 1, value_ptr(spotLight.diffuse)); | |
glUniform3fv(glGetUniformLocation(shader.program, "spotLight.specular"), 1, value_ptr(spotLight.specular)); | |
glUniform3fv(glGetUniformLocation(shader.program, "spotLight.position"), 1, value_ptr(spotLight.position)); | |
glUniform1f(glGetUniformLocation(shader.program, "spotLight.attenuation.constant"), spotLight.attenuation.constant); | |
glUniform1f(glGetUniformLocation(shader.program, "spotLight.attenuation.linear"), spotLight.attenuation.linear); | |
glUniform1f(glGetUniformLocation(shader.program, "spotLight.attenuation.quadratic"), spotLight.attenuation.quadratic); | |
glUniform3fv(glGetUniformLocation(shader.program, "spotLight.position"), 1, value_ptr(spotLight.position)); | |
glUniform3fv(glGetUniformLocation(shader.program, "spotLight.direction"), 1, value_ptr(spotLight.direction)); | |
glUniform1f(glGetUniformLocation(shader.program, "spotLight.innerCutoffAngle"), glm::cos(glm::radians(spotLight.innerCutoffAngle))); | |
glUniform1f(glGetUniformLocation(shader.program, "spotLight.outerCutoffAngle"), glm::cos(glm::radians(spotLight.outerCutoffAngle))); | |
glActiveTexture(GL_TEXTURE0); | |
glBindTexture(GL_TEXTURE_2D, material.diffuseMap); | |
glActiveTexture(GL_TEXTURE1); | |
glBindTexture(GL_TEXTURE_2D, material.specularMap); | |
for (auto i = 0; i < 10; i++) | |
{ | |
glm::mat4 model; | |
model = translate(model, cubePositions[i]); | |
model = rotate(model, 0.f, glm::vec3(1.0f, 0.3f, 0.5f)); | |
auto MVP = projectionMatrix * camera.viewMatrix * model; | |
glUniformMatrix4fv(glGetUniformLocation(shader.program, "MVP"), 1, GL_FALSE, value_ptr(MVP)); | |
glUniformMatrix4fv(glGetUniformLocation(shader.program, "modelMatrix"), 1, GL_FALSE, value_ptr(model)); | |
cube.render(); | |
} | |
glm::mat4 m; | |
m = translate(m, glm::vec3(0.0f, -1.75f, -2.0f)); | |
m = scale(m, glm::vec3(0.05f, 0.05f, 0.05f)); | |
auto MVP = projectionMatrix * camera.viewMatrix * m; | |
glUniformMatrix4fv(glGetUniformLocation(shader.program, "MVP"), 1, GL_FALSE, value_ptr(MVP)); | |
glUniformMatrix4fv(glGetUniformLocation(shader.program, "modelMatrix"), 1, GL_FALSE, value_ptr(m)); | |
model.draw(shader); | |
} | |
void renderPointLight() | |
{ | |
lightShader.use(); | |
for (auto i = 0; i < 3; i++) { | |
glm::mat4 model; | |
model = translate(model, pointLight[i].position); | |
auto MVP = projectionMatrix * camera.viewMatrix * model; | |
glUniformMatrix4fv(glGetUniformLocation(lightShader.program, "MVP"), 1, GL_FALSE, value_ptr(MVP)); | |
glUniform3fv(glGetUniformLocation(lightShader.program, "pointLight.ambient"), 1, value_ptr(pointLight[i].ambient)); | |
glUniform3fv(glGetUniformLocation(lightShader.program, "pointLight.diffuse"), 1, value_ptr(pointLight[i].diffuse)); | |
glUniform3fv(glGetUniformLocation(lightShader.program, "pointLight.specular"), 1, value_ptr(pointLight[i].specular)); | |
cubeLamp.render(); | |
} | |
} | |
void updateSimulation() | |
{ | |
frameTiming.updateDeltaTime(); | |
camera.viewMatrix = lookAt(camera.position, camera.position + camera.front, camera.up); | |
moveCamera(); | |
} | |
void renderSimulation() | |
{ | |
instancedRender(); | |
//renderObject(); | |
//renderPointLight(); | |
} | |
int main() | |
{ | |
glfwInit(); | |
//glfwWindowHint(GLFW_SAMPLES, 4); | |
glfwWindowHint(GLFW_SRGB_CAPABLE, true); | |
window = glfwCreateWindow(static_cast<int>(WIDTH), static_cast<int>(HEIGHT), TITLE, nullptr, nullptr); | |
glfwMakeContextCurrent(window); | |
glfwSetKeyCallback(window, key_callback); | |
glfwSetCursorPos(window, WIDTH / 2, HEIGHT / 2); | |
glfwSetCursorPosCallback(window, mouse_callback); | |
glewExperimental = GL_TRUE; | |
glewInit(); | |
glEnable(GL_FRAMEBUFFER_SRGB); | |
//glEnable(GL_MULTISAMPLE); | |
glEnable(GL_DEPTH_TEST); | |
glDepthFunc(GL_LESS); | |
//initSimulation(); | |
glClearColor(0.f, 0.f, 0.f, 1.f); | |
initInstancedRender(); | |
while (!glfwWindowShouldClose(window)) | |
{ | |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); | |
updateSimulation(); | |
renderSimulation(); | |
glfwSwapBuffers(window); | |
glfwPollEvents(); | |
} | |
glfwTerminate(); | |
return 0; | |
} | |
void key_callback(GLFWwindow* window, int key, int /*scancode*/, int action, int /*mode*/) | |
{ | |
if (action == GLFW_PRESS) | |
{ | |
input.keyPressedStates[key] = true; | |
} | |
else if (action == GLFW_RELEASE) | |
{ | |
input.keyPressedStates[key] = false; | |
} | |
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) | |
glfwSetWindowShouldClose(window, GL_TRUE); | |
} | |
void mouse_callback(GLFWwindow* /*window*/, double xpos, double ypos) | |
{ | |
if (input.mouseInitiallyMoved) | |
{ | |
input.previousMousePosX = xpos; | |
input.previousMousePosY = ypos; | |
input.mouseInitiallyMoved = false; | |
} | |
float xOffset = (xpos - input.previousMousePosX) * input.mouseSensitivity; | |
float yOffset = (input.previousMousePosY - ypos) * input.mouseSensitivity; | |
input.previousMousePosX = xpos; | |
input.previousMousePosY = ypos; | |
orientation.yaw += xOffset; | |
orientation.pitch += yOffset; | |
if (orientation.pitch > 89.f) | |
orientation.pitch = 89.f; | |
else if (orientation.pitch < -89.f) | |
{ | |
orientation.pitch = -89.f; | |
} | |
glm::vec3 front; | |
front.x = cos(glm::radians(orientation.yaw)) * cos(glm::radians(orientation.pitch)); | |
front.y = sin(glm::radians(orientation.pitch)); | |
front.z = sin(glm::radians(orientation.yaw)) * cos(glm::radians(orientation.pitch)); | |
camera.front = normalize(front); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment