switch-examples/graphics/opengl/dynamic_resolution/source/main.cpp

568 lines
17 KiB
C++

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <switch.h>
#include <EGL/egl.h> // EGL library
#include <EGL/eglext.h> // EGL extensions
#include <glad/glad.h> // glad library (OpenGL loader)
// GLM headers
#define GLM_FORCE_PURE
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/rotate_vector.hpp>
// ( ͡° ͜ʖ ͡°) mesh data
#include "lenny.h"
constexpr uint32_t NUMOBJECTS = 64;
constexpr auto TAU = glm::two_pi<float>();
//-----------------------------------------------------------------------------
// nxlink support
//-----------------------------------------------------------------------------
#ifndef ENABLE_NXLINK
#define TRACE(fmt,...) ((void)0)
#else
#include <unistd.h>
#define TRACE(fmt,...) printf("%s: " fmt "\n", __PRETTY_FUNCTION__, ## __VA_ARGS__)
static int s_nxlinkSock = -1;
static void initNxLink()
{
if (R_FAILED(socketInitializeDefault()))
return;
s_nxlinkSock = nxlinkStdio();
if (s_nxlinkSock >= 0)
TRACE("printf output now goes to nxlink server");
else
socketExit();
}
static void deinitNxLink()
{
if (s_nxlinkSock >= 0)
{
close(s_nxlinkSock);
socketExit();
s_nxlinkSock = -1;
}
}
extern "C" void userAppInit()
{
initNxLink();
}
extern "C" void userAppExit()
{
deinitNxLink();
}
#endif
//-----------------------------------------------------------------------------
// EGL initialization
//-----------------------------------------------------------------------------
static EGLDisplay s_display;
static EGLContext s_context;
static EGLSurface s_surface;
static bool initEgl(NWindow* win)
{
// Connect to the EGL default display
s_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (!s_display)
{
TRACE("Could not connect to display! error: %d", eglGetError());
goto _fail0;
}
// Initialize the EGL display connection
eglInitialize(s_display, nullptr, nullptr);
// Select OpenGL (Core) as the desired graphics API
if (eglBindAPI(EGL_OPENGL_API) == EGL_FALSE)
{
TRACE("Could not set API! error: %d", eglGetError());
goto _fail1;
}
// Get an appropriate EGL framebuffer configuration
EGLConfig config;
EGLint numConfigs;
static const EGLint framebufferAttributeList[] =
{
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_STENCIL_SIZE, 8,
EGL_NONE
};
eglChooseConfig(s_display, framebufferAttributeList, &config, 1, &numConfigs);
if (numConfigs == 0)
{
TRACE("No config found! error: %d", eglGetError());
goto _fail1;
}
// Create an EGL window surface
s_surface = eglCreateWindowSurface(s_display, config, win, nullptr);
if (!s_surface)
{
TRACE("Surface creation failed! error: %d", eglGetError());
goto _fail1;
}
// Create an EGL rendering context
static const EGLint contextAttributeList[] =
{
EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR,
EGL_CONTEXT_MAJOR_VERSION_KHR, 4,
EGL_CONTEXT_MINOR_VERSION_KHR, 3,
EGL_NONE
};
s_context = eglCreateContext(s_display, config, EGL_NO_CONTEXT, contextAttributeList);
if (!s_context)
{
TRACE("Context creation failed! error: %d", eglGetError());
goto _fail2;
}
// Connect the context to the surface
eglMakeCurrent(s_display, s_surface, s_surface, s_context);
return true;
_fail2:
eglDestroySurface(s_display, s_surface);
s_surface = nullptr;
_fail1:
eglTerminate(s_display);
s_display = nullptr;
_fail0:
return false;
}
static void deinitEgl()
{
if (s_display)
{
eglMakeCurrent(s_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (s_context)
{
eglDestroyContext(s_display, s_context);
s_context = nullptr;
}
if (s_surface)
{
eglDestroySurface(s_display, s_surface);
s_surface = nullptr;
}
eglTerminate(s_display);
s_display = nullptr;
}
}
//-----------------------------------------------------------------------------
// Main program
//-----------------------------------------------------------------------------
static void setMesaConfig()
{
// Uncomment below to disable error checking and save CPU time (useful for production):
//setenv("MESA_NO_ERROR", "1", 1);
// Uncomment below to enable Mesa logging:
//setenv("EGL_LOG_LEVEL", "debug", 1);
//setenv("MESA_VERBOSE", "all", 1);
//setenv("NOUVEAU_MESA_DEBUG", "1", 1);
// Uncomment below to enable shader debugging in Nouveau:
//setenv("NV50_PROG_OPTIMIZE", "0", 1);
//setenv("NV50_PROG_DEBUG", "1", 1);
//setenv("NV50_PROG_CHIPSET", "0x120", 1);
}
static const char* const vertexShaderSource = R"text(
#version 330 core
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in mat4 inMdlMtx;
out vec4 vtxColor;
out vec4 vtxNormalQuat;
out vec3 vtxView;
uniform mat4 mdlvMtx;
uniform mat4 projMtx;
void main()
{
// Calculate position
vec4 pos = mdlvMtx * inMdlMtx * vec4(inPos, 1.0);
vtxView = -pos.xyz;
gl_Position = projMtx * pos;
// Calculate normalquat
vec3 normal = normalize(mat3(mdlvMtx) * inNormal);
float z = (1.0 + normal.z) / 2.0;
vtxNormalQuat = vec4(1.0, 0.0, 0.0, 0.0);
if (z > 0.0)
{
vtxNormalQuat.z = sqrt(z);
vtxNormalQuat.xy = normal.xy / (2.0 * vtxNormalQuat.z);
}
// Calculate color
vtxColor = vec4(1.0);
}
)text";
static const char* const fragmentShaderSource = R"text(
#version 330 core
in vec4 vtxColor;
in vec4 vtxNormalQuat;
in vec3 vtxView;
out vec4 fragColor;
uniform vec4 lightPos;
uniform vec3 ambient;
uniform vec3 diffuse;
uniform vec4 specular; // w component is shininess
// Rotate the vector v by the quaternion q
vec3 quatrotate(vec4 q, vec3 v)
{
return v + 2.0*cross(q.xyz, cross(q.xyz, v) + q.w*v);
}
void main()
{
// Extract normal from quaternion
vec4 normquat = normalize(vtxNormalQuat);
vec3 normal = quatrotate(normquat, vec3(0.0, 0.0, 1.0));
vec3 lightVec;
if (lightPos.w != 0.0)
lightVec = normalize(lightPos.xyz + vtxView);
else
lightVec = normalize(lightPos.xyz);
vec3 viewVec = normalize(vtxView);
vec3 halfVec = normalize(viewVec + lightVec);
float diffuseFactor = max(dot(lightVec, normal), 0.0);
float specularFactor = pow(max(dot(normal, halfVec), 0.0), specular.w);
vec3 fragLightColor = ambient + diffuseFactor*diffuse + specularFactor*specular.xyz;
fragColor = vec4(min(fragLightColor, 1.0), 1.0);
}
)text";
static GLuint createAndCompileShader(GLenum type, const char* source)
{
GLint success;
GLchar msg[512];
GLuint handle = glCreateShader(type);
if (!handle)
{
TRACE("%u: cannot create shader", type);
return 0;
}
glShaderSource(handle, 1, &source, nullptr);
glCompileShader(handle);
glGetShaderiv(handle, GL_COMPILE_STATUS, &success);
if (success == GL_FALSE)
{
glGetShaderInfoLog(handle, sizeof(msg), nullptr, msg);
TRACE("%u: %s\n", type, msg);
glDeleteShader(handle);
return 0;
}
return handle;
}
// Per-instance data
struct Instance
{
glm::mat4 mdlMtx;
};
static GLuint s_program;
static GLuint s_vao, s_vbo, s_instance_vbo;
static GLint loc_mdlvMtx, loc_projMtx;
static GLint loc_lightPos, loc_ambient, loc_diffuse, loc_specular;
static u64 s_startTicks;
static void sceneInit()
{
GLint vsh = createAndCompileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLint fsh = createAndCompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
s_program = glCreateProgram();
glAttachShader(s_program, vsh);
glAttachShader(s_program, fsh);
glLinkProgram(s_program);
GLint success;
glGetProgramiv(s_program, GL_LINK_STATUS, &success);
if (success == GL_FALSE)
{
char buf[512];
glGetProgramInfoLog(s_program, sizeof(buf), nullptr, buf);
TRACE("Link error: %s", buf);
}
glDeleteShader(vsh);
glDeleteShader(fsh);
loc_mdlvMtx = glGetUniformLocation(s_program, "mdlvMtx");
loc_projMtx = glGetUniformLocation(s_program, "projMtx");
loc_lightPos = glGetUniformLocation(s_program, "lightPos");
loc_ambient = glGetUniformLocation(s_program, "ambient");
loc_diffuse = glGetUniformLocation(s_program, "diffuse");
loc_specular = glGetUniformLocation(s_program, "specular");
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glGenVertexArrays(1, &s_vao);
glGenBuffers(1, &s_vbo);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(s_vao);
glBindBuffer(GL_ARRAY_BUFFER, s_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(lennyVertices), lennyVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(lennyVertex), (void*)offsetof(lennyVertex, x));
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(lennyVertex), (void*)offsetof(lennyVertex, nx));
glEnableVertexAttribArray(1);
glGenBuffers(1, &s_instance_vbo);
glBindBuffer(GL_ARRAY_BUFFER, s_instance_vbo);
// Generate the per-instance data: the instances will form a sine wave.
Instance* instances = new Instance[NUMOBJECTS];
for (size_t i = 0; i < NUMOBJECTS; i ++)
{
float x = float(i) / (NUMOBJECTS-1);
float a = x*2.0f-1.0f;
instances[i].mdlMtx = glm::translate(glm::mat4{1.0f}, glm::vec3{a*4.0f, 1.5f*sinf(x*TAU), a});
instances[i].mdlMtx = glm::scale(instances[i].mdlMtx, glm::vec3{2.0f});
}
// Upload the instance data
glBufferData(GL_ARRAY_BUFFER, sizeof(Instance)*NUMOBJECTS, instances, GL_STATIC_DRAW);
delete[] instances;
// Set up per-instance attributes
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Instance), (void*)(offsetof(Instance, mdlMtx)+0*sizeof(glm::vec4)));
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(Instance), (void*)(offsetof(Instance, mdlMtx)+1*sizeof(glm::vec4)));
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(Instance), (void*)(offsetof(Instance, mdlMtx)+2*sizeof(glm::vec4)));
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(Instance), (void*)(offsetof(Instance, mdlMtx)+3*sizeof(glm::vec4)));
glVertexAttribDivisor(2, 1);
glVertexAttribDivisor(3, 1);
glVertexAttribDivisor(4, 1);
glVertexAttribDivisor(5, 1);
glEnableVertexAttribArray(2);
glEnableVertexAttribArray(3);
glEnableVertexAttribArray(4);
glEnableVertexAttribArray(5);
// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);
// Uniforms
glUseProgram(s_program);
auto projMtx = glm::perspective(40.0f*TAU/360.0f, 16.0f/9.0f, 0.01f, 1000.0f);
glUniformMatrix4fv(loc_projMtx, 1, GL_FALSE, glm::value_ptr(projMtx));
glUniform4f(loc_lightPos, 0.0f, 0.0f, -0.5f, 1.0f);
glUniform3f(loc_ambient, 0.1f, 0.1f, 0.1f);
glUniform3f(loc_diffuse, 0.4f, 0.4f, 0.4f);
glUniform4f(loc_specular, 0.5f, 0.5f, 0.5f, 20.0f);
s_startTicks = armGetSystemTick();
}
static float getTime()
{
u64 elapsed = armGetSystemTick() - s_startTicks;
return (elapsed * 625 / 12) / 1000000000.0;
}
static float s_updTime = 0.0f;
static float s_cameraAngle = 0.0f;
static glm::vec3 s_cameraPos{0.0f, 0.0f, 3.0f};
template <typename T>
static inline T fract(T x)
{
return x - std::floor(x);
}
static void sceneUpdate(u32 kHeld)
{
float curTime = getTime();
float deltaTime = curTime - s_updTime;
s_updTime = curTime;
if (kHeld & HidNpadButton_AnyLeft)
s_cameraAngle = fract(s_cameraAngle - deltaTime/4);
else if (kHeld & HidNpadButton_AnyRight)
s_cameraAngle = fract(s_cameraAngle + deltaTime/4);
if (kHeld & (HidNpadButton_AnyUp|HidNpadButton_AnyDown))
{
glm::vec3 front = deltaTime * glm::rotate(glm::vec3{0.0f, 0.0f, -1.0f}, s_cameraAngle * TAU, glm::vec3{0.0f, -1.0f, 0.0f});
if (kHeld & HidNpadButton_AnyUp)
s_cameraPos += front;
else if (kHeld & HidNpadButton_AnyDown)
s_cameraPos -= front;
}
glm::mat4 mdlvMtx{1.0};
mdlvMtx = glm::rotate(mdlvMtx, s_cameraAngle * TAU, glm::vec3{0.0f, 1.0f, 0.0f});
mdlvMtx = glm::translate(mdlvMtx, -s_cameraPos);
glUniformMatrix4fv(loc_mdlvMtx, 1, GL_FALSE, glm::value_ptr(mdlvMtx));
}
static void configureResolution(NWindow* win, bool halved)
{
int width, height;
// Calculate the target resolution depending on the operation mode:
// - In handheld mode, we render at 720p (which is the native screen resolution).
// - In docked mode, we render at full 1080p (which is outputted to a compatible HDTV screen).
switch (appletGetOperationMode())
{
default:
case AppletOperationMode_Handheld:
width = 1280;
height = 720;
break;
case AppletOperationMode_Docked:
width = 1920;
height = 1080;
break;
}
// As an additional demonstration, we also demonstrate what happens
// when the rendering resolution doesn't match the native display resolution
// by allowing the user to hold A to halve the rendering resolution.
if (halved)
{
width /= 2;
height /= 2;
}
// Apply the resolution, and configure the correct GL viewport.
// We want to render to the top left corner of the framebuffer (other areas will
// remain unused when rendering at a smaller resolution than the framebuffer).
// Note that glViewport expects the coordinates of the bottom-left corner of
// the viewport, so we have to calculate that too.
nwindowSetCrop(win, 0, 0, width, height);
glViewport(0, 1080-height, width, height);
}
static void sceneRender()
{
glClearColor(0x68/255.0f, 0xB0/255.0f, 0xD8/255.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// draw our ( ͡° ͜ʖ ͡°) world
glBindVertexArray(s_vao); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glDrawArraysInstanced(GL_TRIANGLES, 0, lennyVerticesCount, NUMOBJECTS);
}
static void sceneExit()
{
glDeleteBuffers(1, &s_instance_vbo);
glDeleteBuffers(1, &s_vbo);
glDeleteVertexArrays(1, &s_vao);
glDeleteProgram(s_program);
}
int main(int argc, char* argv[])
{
// Set mesa configuration (useful for debugging)
setMesaConfig();
// Retrieve the default window and configure its dimensions (1080p)
NWindow* win = nwindowGetDefault();
nwindowSetDimensions(win, 1920, 1080);
// Initialize EGL on the default window
if (!initEgl(win))
return EXIT_FAILURE;
// Load OpenGL routines using glad
gladLoadGL();
// Initialize our scene
sceneInit();
// Configure our supported input layout: a single player with standard controller styles
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
// Initialize the default gamepad (which reads handheld mode inputs as well as the first connected controller)
PadState pad;
padInitializeDefault(&pad);
// Main graphics loop
while (appletMainLoop())
{
// Get and process input
padUpdate(&pad);
u32 kDown = padGetButtonsDown(&pad);
u32 kHeld = padGetButtons(&pad);
if (kDown & HidNpadButton_Plus)
break;
bool shouldHalveResolution = !!(kHeld & HidNpadButton_A);
// Configure the resolution used to render the scene, which
// will be different in handheld mode/docked mode.
// As an additional demonstration, when holding A we render the scene
// at half the original resolution.
configureResolution(win, shouldHalveResolution);
// Update our scene
sceneUpdate(kHeld);
// Render stuff!
sceneRender();
eglSwapBuffers(s_display, s_surface);
}
// Deinitialize our scene
sceneExit();
// Deinitialize EGL
deinitEgl();
return EXIT_SUCCESS;
}