// PBR model viewer. Based on Foxotron by @gargaj + cce/Peisik (UNLICENSE). // - rlyeh, public domain. // // @todo: Middle mouse button to pan camera @todo #include "v4k.h" #if is(tcc) && !is(win32) // @todo: remove this & test on linux int log2_64 (uint64_t value) { const int tab64[64] = { 63, 0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61, 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4, 62, 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21, 56, 45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5}; value |= value >> 1; value |= value >> 2; value |= value >> 4; value |= value >> 8; value |= value >> 16; value |= value >> 32; return tab64[((uint64_t)((value - (value >> 1))*0x07EDD5E59A4E28C2)) >> 58]; } #define log2 log2_64 #endif // ----------------------------------------------------------------------------- // textures texture_t *LoadTextureRGBA8( const char *pathfile, unsigned flags ) { int flags_hdr = strendi(pathfile, ".hdr") ? TEXTURE_FLOAT | TEXTURE_RGBA : 0; texture_t t = texture(pathfile, flags | TEXTURE_LINEAR | TEXTURE_MIPMAPS | TEXTURE_REPEAT | flags_hdr); if( t.id == texture_checker().id ) { return NULL; } texture_t *tex = CALLOC(1, sizeof(texture_t)); *tex = t; return tex; } // ----------------------------------------------------------------------------- // models typedef struct Mesh { GLuint vao, vbo, ibo; int vert_stride; void *vert_stream; int num_verts, num_tris; int material_idx; bool transparent; vec3 aabb_min, aabb_max; } Mesh; typedef struct Model { array(Mesh) meshes; array(pbr_material_t) materials; unsigned shader; } Model; bool ModelLoad( Model *G, const char *_path ); void ModelDestroy( Model *G ); void ModelRebind( Model *G, unsigned shader ); void ModelRender( Model *G, const mat44 _worldRootMatrix ); void ModelDestroy( Model *G) { for( int i = 0, end = array_count(G->materials); i < end; ++i ) { pbr_material_destroy(&G->materials[i]); } array_free(G->materials); for( int i = 0, end = array_count(G->meshes); i < end; ++i ) { Mesh *it = &G->meshes[i]; glDeleteBuffers( 1, &it->ibo ); glDeleteBuffers( 1, &it->vbo ); glDeleteVertexArrays( 1, &it->vao ); } array_free(G->meshes); } bool ModelLoad( Model *G, const char *_path ) { ModelDestroy(G); // ------------------------------------------------------------------------- Model g = {0}; *G = g; model_t m = model(_path, 0); int scn_num_meshes = m.num_meshes; int scn_num_materials = array_count(m.materials); // ------------------------------------------------------------------------- for( int i = 0; i < scn_num_materials; i++ ) { const char *name = m.materials[i].name; PRINTF("Loading material %d/%d: '%s'\n", i + 1, scn_num_materials, name); pbr_material_t mt; pbr_material(&mt, name); array_push(G->materials, mt); } for( int i = 0; i < scn_num_meshes; i++ ) { PRINTF("Loading mesh %d/%d\n", i + 1, scn_num_meshes); int verts = m.num_verts; int faces = m.num_triangles; unsigned material_index = 0; // &m.iqm->meshes[i].material; // aiGetMeshMaterialIndex(scn_mesh[i]); bool has_data = verts && faces; if( !has_data ) { continue; } PRINTF("Loading mesh v%d/f%d\n", verts, faces); Mesh mesh = { 0 }; mesh.vao = m.vao; mesh.vbo = m.vbo; mesh.ibo = m.ibo; mat44 id; id44(id); mesh.aabb_min = model_aabb(m, id).min; mesh.aabb_max = model_aabb(m, id).max; // p3 n3 t3 b3 u2 mesh.vert_stride = m.stride; mesh.vert_stream = m.verts; mesh.num_verts = verts; mesh.num_tris = faces; mesh.material_idx = material_index; // By importing materials before meshes we can investigate whether a mesh is transparent and flag it as such. const pbr_material_t* mtl = G->materials ? &G->materials[mesh.material_idx] : NULL; mesh.transparent = false; if( mtl ) { mesh.transparent |= mtl->albedo .texture ? mtl->albedo .texture->transparent : mtl->albedo .color.a < 1.0f; mesh.transparent |= mtl->diffuse.texture ? mtl->diffuse.texture->transparent : mtl->diffuse.color.a < 1.0f; } array_push(G->meshes, mesh); } #if 0 G->mGlobalAmbient = vec4( 0.3,0.3,0.3,0.3 ); int scn_num_lights = 0; for( int i = 0; i < scn_num_lights; i++ ) { PRINTF("Loading light %d/%d\n", i + 1, scn_num_lights); vec4 *color = aiGetLightColor(scn_light[i]); char *type = aiGetLightType(scn_light[i]); if( 0 == strcmp(type, "AMBIENT") ) { memcpy( &G->mGlobalAmbient, &color->r, sizeof( float ) * 4 ); } else { // @todo } } #endif return true; } void ModelRender( Model *G, const mat44 _worldRootMatrix ) { unsigned _shader = G->shader; shader_bind( _shader ); shader_vec4("global_ambient", vec4(1,1,1,1)); // unused // loop thrice: first opaque, then transparent backface, then transparent frontface for(int j = 0; j < 3; ++j) { bool bTransparentPass = j > 0; if(bTransparentPass) { glEnable( GL_BLEND ); glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); glCullFace( j == 1 ? GL_FRONT : GL_BACK ); // glDepthMask( GL_FALSE); } mat44 mat_world; copy44(mat_world, _worldRootMatrix); // @fixme mMatrices[ node.mID ] * _worldRootMatrix shader_mat44( "mat_world", mat_world ); for( int i = 0, end = array_count(G->meshes); i < end; i++ ) { const Mesh *mesh = &G->meshes[ i ]; // Postpone rendering transparent meshes if(mesh->transparent != bTransparentPass) continue; const pbr_material_t *material = &G->materials[ mesh->material_idx ]; shader_colormap( "map_diffuse", material->diffuse ); shader_colormap( "map_normals", material->normals ); shader_colormap( "map_specular", material->specular ); shader_colormap( "map_albedo", material->albedo ); shader_colormap( "map_roughness", material->roughness ); shader_colormap( "map_metallic", material->metallic ); shader_colormap( "map_ao", material->ao ); shader_colormap( "map_ambient", material->ambient ); shader_colormap( "map_emissive", material->emissive ); shader_float( "specular_shininess", material->specular_shininess ); // unused, basic_specgloss.fs only shader_vec2( "resolution", vec2(window_width(),window_height())); glActiveTexture(GL_TEXTURE0); // be nice to Mesa before rendering glBindVertexArray( mesh->vao ); glDrawElements( GL_TRIANGLES, mesh->num_tris * 3, GL_UNSIGNED_INT, NULL ); } if(bTransparentPass) { glDisable( GL_BLEND ); // glDepthMask( GL_TRUE ); } } //glBindVertexArray( 0 ); //glUseProgram( 0 ); } static void G_SetupVertexArray( unsigned _shader, const char *name, int stride, int num_floats, int *offset, int opt_location ) { int location = opt_location >= 0 ? opt_location : glGetAttribLocation( _shader, name ); if( location >= 0 ) { glVertexAttribPointer( location, num_floats, GL_FLOAT, GL_FALSE, stride, (GLvoid *)(uintptr_t)(*offset) ); glEnableVertexAttribArray( location ); } *offset += num_floats * sizeof( GLfloat ); } void ModelRebind( Model *G, unsigned _shader ) { shader_bind(_shader); if(_shader == G->shader) return; G->shader = _shader; for( int i = 0, end = array_count(G->meshes); i < end; i++ ) { const Mesh *mesh = &G->meshes[ i ]; glBindVertexArray( mesh->vao ); glBindBuffer( GL_ARRAY_BUFFER, mesh->vbo ); glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh->ibo ); glDisableVertexAttribArray( 0 ); glDisableVertexAttribArray( 1 ); glDisableVertexAttribArray( 2 ); glDisableVertexAttribArray( 3 ); glDisableVertexAttribArray( 4 ); glDisableVertexAttribArray( 5 ); glDisableVertexAttribArray( 6 ); glDisableVertexAttribArray( 7 ); int offset = 0, stride = mesh->vert_stride; G_SetupVertexArray( _shader, "in_pos", stride, 3, &offset, -1/*0*/ ); G_SetupVertexArray( _shader, "in_texcoord", stride, 2, &offset, -1/*1*/ ); G_SetupVertexArray( _shader, "in_normal", stride, 3, &offset, -1/*2*/ ); G_SetupVertexArray( _shader, "in_tangent", stride, 4, &offset, -1/*3*/ ); //glBindVertexArray( 0 ); } } // ----------------------------------------------------------------------------- // skyboxes // Extracts single key value from an HDRLabs IBL file. Returns an empty string on error. static const char *ibl_readkey( const char* pathfile, const char* key ) { char *data = vfs_read(pathfile); if( data ) { const char *found = strstr(data, va("%s=", key)); if( found ) return found + strlen(key) + 1; found = strstr(data, va("%s =", key)); if( found ) return found + strlen(key) + 2; } return ""; } typedef struct Skybox { vec3 sunColor; float sunYaw, sunPitch; // ibl settings texture_t *reflection; // reflection map (hdr) texture_t *env; // irradiance map (env) } Skybox; Skybox g_skybox = { {1,1,1} }; void SkyboxDestroy( Skybox *s ) { if( s->reflection ) texture_destroy( s->reflection ); if( s->env ) texture_destroy( s->env ); *s = (Skybox){0}; } bool SkyboxLoad( Skybox *s, const char **slots ) { // hdr,env,ibl SkyboxDestroy( s ); const char* reflectionPath = slots[0]; const char* envPath = slots[1]; const char* iblPath = slots[2]; // unsigned invalid = texture_checker().id; // Reflection map if( reflectionPath ) { if( (s->reflection = LoadTextureRGBA8( reflectionPath, TEXTURE_SRGB )) != NULL ) { glBindTexture( GL_TEXTURE_2D, s->reflection->id ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } } // Irradiance map if( envPath ) { if( (s->env = LoadTextureRGBA8( envPath, TEXTURE_SRGB )) != NULL ) { glBindTexture( GL_TEXTURE_2D, s->env->id ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } } // Sun color & direction from .ibl file s->sunColor = vec3(1,1,1); s->sunYaw = 0, s->sunPitch = 0; if( iblPath ) { vec3 sc; if( 3 == sscanf(ibl_readkey(iblPath, "SUNcolor"), "%f,%f,%f", &sc.x, &sc.y, &sc.z) ) { s->sunColor = scale3(sc, 1/255.f); } vec2 uv = vec2(atof(ibl_readkey(iblPath, "SUNu")), atof(ibl_readkey(iblPath, "SUNv"))); if( len2(uv) > 0 ) { s->sunYaw = C_PI * (-2. * uv.x + 0.5f); s->sunPitch = (0.5f - uv.y) * C_PI; } } return s->reflection && s->env; } // ----------------------------------------------------------------------------- // main const char *shader_names[] = {"Physically Based", "Basic SpecGloss" }; const char *shaders[][2] = { // name, vs, fs { "Shaders/pbr.vs", "Shaders/pbr.fs" }, { "Shaders/basic_specgloss.vs", "Shaders/basic_specgloss.fs" } }; const char *skyboxes[][3] = { // reflection, env, metadata {"hdr/Tokyo_BigSight_1k.hdr","hdr/Tokyo_BigSight_Env.hdr","hdr/Tokyo_BigSight.ibl"}, {"hdr/GCanyon_C_YumaPoint_1k.hdr","hdr/GCanyon_C_YumaPoint_Env.hdr","hdr/GCanyon_C_YumaPoint.ibl"}, {"hdr/Factory_Catwalk_1k.hdr","hdr/Factory_Catwalk_Env.hdr","hdr/Factory_Catwalk.ibl"}, {"hdr/MonValley_G_DirtRoad_1k.hdr","hdr/MonValley_G_DirtRoad_Env.hdr","hdr/MonValley_G_DirtRoad.ibl"}, {"hdr/Shiodome_Stairs_1k.hdr","hdr/Shiodome_Stairs_Env.hdr","hdr/Shiodome_Stairs.ibl"}, }; Model gModel; unsigned gShader = ~0u; unsigned gShaderConfig = ~0u; bool LoadShaderConfig( int slot ) { // name,vs,fs unsigned newShader = shader( vfs_read(shaders[slot][0]), vfs_read(shaders[slot][1]), NULL, NULL, NULL ); if( newShader == ~0u ) return false; shader_destroy( gShader ); gShaderConfig = slot; gShader = newShader; return true; } void camera_fit(camera_t *cam) { vec3 target = scale3( add3( gModel.meshes[0].aabb_min, gModel.meshes[0].aabb_max ), 0.5f); float distance = len3( sub3( gModel.meshes[0].aabb_max, gModel.meshes[0].aabb_min ) ) * 0.85f; cam->position = add3(target, scale3(norm3(sub3(cam->position,target)), distance)); camera_lookat(cam, vec3(0,0,0)); } int main() { window_create( 75, WINDOW_MSAA2 ); window_title(__FILE__); // load all fx files in all subdirs fx_load("fx**.fs"); LoadShaderConfig( 0 ); brdf_lut(); // ------------------------------------------------------------------------- // Mainloop float model_yaw = 0, model_pitch = 0; float lightYaw = 0.0f; float lightPitch = 0.0f; vec4 skyBackgroundColor = vec4(0.01,0.01,0.02,1); // vec4(1,0,0,1); float skyExposure = 1.0; // plain 'exposure' instead? this is camera related float skyBlur = 0.00; // 0.00 float skyOpacity = 0.99; // 0.99 bool do_wireframe = false; bool do_xzySpace = true; // xzySpace or xyzSpace bool do_flipY = false; const mat44 xzyMatrix = { 1, 0, 0, 0, 0, 0, 1, 0, 0,+1, 0, 0, 0, 0, 0, 1 }; camera_t cam = camera(); cam.speed = 0.1; int firstskyboxes = 0; // 0: tokyo_bigsight SkyboxLoad( &g_skybox, &skyboxes[firstskyboxes][0] ); lightYaw = g_skybox.sunYaw; lightPitch = g_skybox.sunPitch; unsigned skysphereShader = shader( vfs_read("Skyboxes/skysphere.vs"), vfs_read("Skyboxes/skysphere.fs"), NULL, NULL, NULL ); Model skysphere = { 0 }; ModelLoad(&skysphere, "Skyboxes/skysphere.fbx"); ModelRebind(&skysphere, skysphereShader); if( ModelLoad( &gModel, option("--model", "damagedhelmet.gltf") ) ) { ModelRebind( &gModel, gShader ); } cam.position = vec3(+1,0,+1); camera_fit(&cam); static mat44 worldRootXYZ; do_once id44(worldRootXYZ); // mat44( 1.0f ); while( window_swap() && !input(KEY_ESC) ) { if( input(KEY_F5) ) window_reload(); if( input_down( KEY_F ) ) camera_fit(&cam); // --------------------------------------------------------------------- static int fps_mode; if(input_down(KEY_TAB)) { fps_mode ^= 1; camera_fit(&cam); } if(fps_mode) { // fps camera bool active = ui_active() || ui_hover() || gizmo_active() ? false : input(MOUSE_L) || input(MOUSE_M) || input(MOUSE_R); window_cursor( !active ); if( active ) cam.speed = clampf(cam.speed + input_diff(MOUSE_W) / 10, 0.05f, 5.0f); vec2 mouse = scale2(vec2(input_diff(MOUSE_X), -input_diff(MOUSE_Y)), 0.2f * active); vec3 wasdecq = scale3(vec3(input(KEY_D)-input(KEY_A),input(KEY_E)-(input(KEY_C)||input(KEY_Q)),input(KEY_W)-input(KEY_S)), cam.speed); camera_moveby(&cam, wasdecq); camera_fps(&cam, mouse.x,mouse.y); } else { // orbit camera window_cursor( true ); bool active = !ui_active() && !ui_hover() && !gizmo_active(); vec2 inc_mouse = scale2(vec2(input_diff(MOUSE_X), -input_diff(MOUSE_Y)), 0.2f * active * input(MOUSE_L)); float inc_distance = -0.2f * active * input_diff(MOUSE_W); camera_orbit(&cam, inc_mouse.x, inc_mouse.y, inc_distance); // rotate model model_yaw -= input_diff(MOUSE_X) * 0.2f * active * input(MOUSE_R); model_pitch += input_diff(MOUSE_Y) * 0.2f * active * input(MOUSE_R); } // --------------------------------------------------------------------- glClearColor( skyBackgroundColor.r, skyBackgroundColor.g, skyBackgroundColor.b, skyBackgroundColor.a ); glEnable(GL_CULL_FACE); glFrontFace(GL_CCW); // --------------------------------------------------------------------- // Mesh state fx_begin(); profile("PBR Model (bindings)") { ModelRebind( &gModel, gShader ); shader_mat44( "mat_projection", cam.proj ); //cameraPosition = scale3(cameraPosition, gCameraDistance); shader_vec3( "camera_position", cam.position ); vec3 lightDirection = vec3( 0, 0, 1 ); lightDirection = rotatex3( lightDirection, deg(lightPitch) ); lightDirection = rotatey3( lightDirection, deg(lightYaw) ); vec3 fillLightDirection = vec3( 0, 0, 1 ); fillLightDirection = rotatex3( fillLightDirection, deg(lightPitch - 0.4f) ); fillLightDirection = rotatey3( fillLightDirection, deg(lightYaw + 0.8f) ); shader_vec3( "lights[0].direction", lightDirection ); shader_vec3( "lights[0].color", g_skybox.sunColor ); shader_vec3( "lights[1].direction", fillLightDirection ); shader_vec3( "lights[1].color", vec3( 0.5f, 0.5f, 0.5f ) ); shader_vec3( "lights[2].direction", neg3(fillLightDirection) ); shader_vec3( "lights[2].color", vec3( 0.25f, 0.25f, 0.25f ) ); shader_float( "skysphere_rotation", lightYaw - g_skybox.sunYaw ); mat44 viewMatrix, inv_viewMatrix; copy44(viewMatrix, cam.view); invert44( inv_viewMatrix, viewMatrix); shader_mat44( "mat_view", viewMatrix ); shader_mat44( "mat_view_inverse", inv_viewMatrix ); shader_bool( "has_tex_skysphere", g_skybox.reflection != NULL ); shader_bool( "has_tex_skyenv", g_skybox.env != NULL ); if( g_skybox.reflection ) { float mipCount = floor( log2( g_skybox.reflection->h ) ); shader_texture( "tex_skysphere", *g_skybox.reflection ); shader_float( "skysphere_mip_count", mipCount ); } if( g_skybox.env ) { shader_texture( "tex_skyenv", *g_skybox.env ); } shader_texture( "tex_brdf_lut", brdf_lut() ); shader_float( "exposure", skyExposure ); shader_uint( "frame_count", (unsigned)window_frame() ); } // --------------------------------------------------------------------- // Mesh render mat44 M; copy44( M, do_xzySpace ? xzyMatrix : worldRootXYZ ); if( do_flipY ) scale44( M, 1,-1,1 ); rotate44( M, model_yaw, 0,0,1 ); rotate44( M, model_pitch, 1,0,0 ); profile("PBR Model (render)") { ModelRender( &gModel, M ); } profile("PBR Model (wireframe)") { if( do_wireframe ) { glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); glDepthFunc( GL_LEQUAL ); shader_float("exposure", 100.0f ); ModelRender( &gModel, M ); glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); glDepthFunc( GL_LESS ); } } // --------------------------------------------------------------------- // Skysphere render profile("PBR Skybox") { ModelRebind(&skysphere, skysphereShader ); mat44 projview; multiply44x2(projview, cam.proj, cam.view); shader_mat44( "mat_mvp", projview ); shader_bool( "has_tex_skysphere", g_skybox.reflection != NULL ); shader_bool( "has_tex_skyenv", g_skybox.env != NULL ); if( g_skybox.reflection ) { const float mipCount = floor( log2( g_skybox.reflection->h ) ); shader_texture( "tex_skysphere", *g_skybox.reflection ); shader_float( "skysphere_mip_count", mipCount ); } if( g_skybox.env ) { shader_texture( "tex_skyenv", *g_skybox.env ); } shader_vec4( "background_color", skyBackgroundColor ); shader_float( "skysphere_blur", skyBlur ); shader_float( "skysphere_opacity", skyOpacity ); shader_float( "skysphere_rotation", lightYaw - g_skybox.sunYaw ); shader_float( "exposure", skyExposure ); shader_uint( "frame_count", (unsigned)window_frame() ); glDepthFunc( GL_LEQUAL ); ModelRender(&skysphere, worldRootXYZ ); glDepthFunc( GL_LESS ); } fx_end(); // --------------------------------------------------------------------- // UI if( ui_panel( "Viewer", 0 ) ) { ui_bool( "Wireframe", &do_wireframe ); ui_separator(); if( ui_radio("Shader config:", shader_names, countof(shader_names), &gShaderConfig) ) { LoadShaderConfig( gShaderConfig ); ModelRebind(&gModel, gShader ); } ui_separator(); for( int i = 0; i < countof(skyboxes); i++ ) { const char *filename = skyboxes[i][0]; bool selected = !strcmp(g_skybox.reflection->filename, file_name(filename)); if( ui_bool( filename, &selected ) ) { SkyboxLoad( &g_skybox, &skyboxes[i][0] ); lightYaw = g_skybox.sunYaw; lightPitch = g_skybox.sunPitch; } } ui_separator(); ui_float( "Sky exposure", &skyExposure); skyExposure = clampf(skyExposure, 0.1f, 4.0f ); ui_float( "Sky blur", &skyBlur); skyBlur = clampf(skyBlur, 0.0f, 1.0f ); ui_float( "Sky opacity", &skyOpacity); skyOpacity = clampf(skyOpacity, 0.0f, 1.0f ); ui_color4f( "Sky background", (float *) &skyBackgroundColor.x ); ui_separator(); ui_float( "SunLight Yaw", &lightYaw ); ui_float( "SunLight Pitch", &lightPitch ); ui_panel_end(); } if( ui_panel( "Model", 0 ) ) { ui_label(va("Material count: %d", array_count(gModel.materials))); ui_label(va("Mesh count: %d", array_count(gModel.meshes))); int triCount = 0; for( int i = 0, end = array_count(gModel.meshes); i < end; ++i ) triCount += gModel.meshes[i].num_tris; ui_label(va("Triangle count: %d", triCount)); ui_separator(); bool xyzSpace = !do_xzySpace; if( ui_bool( "XYZ space", &xyzSpace ) ) { do_xzySpace = !do_xzySpace; } ui_bool( "XZY space", &do_xzySpace ); ui_bool( "invert Y", &do_flipY ); ui_separator(); for( int i = 0, end = array_count(gModel.materials); i < end; ++i ) { pbr_material_t *it = &gModel.materials[i]; ui_label(va("Name: %s", it->name)); ui_float( "Specular shininess", &it->specular_shininess ); ui_separator(); if(ui_colormap( "Albedo", &it->albedo )) colormap(&it->albedo , app_loadfile(), 1); ui_separator(); if(ui_colormap( "Ambient", &it->ambient )) colormap(&it->ambient , app_loadfile(), 0); ui_separator(); if(ui_colormap( "AO", &it->ao )) colormap(&it->ao , app_loadfile(), 0); ui_separator(); if(ui_colormap( "Diffuse", &it->diffuse )) colormap(&it->diffuse , app_loadfile(), 1); ui_separator(); if(ui_colormap( "Emissive", &it->emissive )) colormap(&it->emissive , app_loadfile(), 1); ui_separator(); if(ui_colormap( "Metallic", &it->metallic )) colormap(&it->metallic , app_loadfile(), 0); ui_separator(); if(ui_colormap( "Normal", &it->normals )) colormap(&it->normals , app_loadfile(), 0); ui_separator(); if(ui_colormap( "Roughness", &it->roughness )) colormap(&it->roughness, app_loadfile(), 0); ui_separator(); if(ui_colormap( "Specular", &it->specular )) colormap(&it->specular , app_loadfile(), 0); } ui_panel_end(); } if( ui_panel("Help", 0)) { if( fps_mode ) { ui_label("TAB: switch to Orbit camera mode"); ui_label("WASD,QEC: move camera"); ui_label("Drag + Mouse Button: camera freelook"); } else { ui_label("TAB: switch to FPS camera mode"); ui_label("Drag + Left Mouse Button: orbit camera"); ui_label("Drag + Right Mouse Button: rotate model"); ui_label("Mouse wheel: camera distance"); } ui_label("F: center view"); ui_panel_end(); } } }