OpenGL ES 3.0 for phones without Vulkan; the old-GPU renderer draws the grid; site fonts

- Android: Vulkan 1.3 if the phone has it, else OpenGL ES 3.0 (the
  fallback renderer, which now also runs on ES); the APK no longer
  needs Vulkan to install
- The fallback renderer draws sprites and streaks: the grid and axes
- The website ships its fonts (Inter, JetBrains Mono): no more falling
  back to whatever the visitor has
- Android: link the log library (GitHub's build)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
Nexoniarz 2026-10-19 22:22:55 +02:00
commit 5e84ef0e13
13 changed files with 348 additions and 53 deletions

View file

@ -3,7 +3,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.github.nexoniarz.fry3dx" package="io.github.nexoniarz.fry3dx"
android:installLocation="auto"> android:installLocation="auto">
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x403000" android:required="true" /> <!-- Vulkan 1.3 if the phone has it, otherwise OpenGL ES 3.0 (a simpler look) -->
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x403000" android:required="false" />
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application <application
android:label="Fry3DX" android:label="Fry3DX"

View file

@ -25,13 +25,13 @@ window.FRY = {
{ icon: "◫", title: "Real shape editing", text: "Points, edges and faces. Extrude, inset, round edges, loop cut, knife, merge. Shift+click picks a whole loop, Shift+Ctrl+click a ring." }, { icon: "◫", title: "Real shape editing", text: "Points, edges and faces. Extrude, inset, round edges, loop cut, knife, merge. Shift+click picks a whole loop, Shift+Ctrl+click a ring." },
{ icon: "◎", title: "Holes for screws and nuts", text: "Round, rectangle, triangle or hexagon. M2 to M8 screw and nut sizes ready, with a little room added so the print actually fits." }, { icon: "◎", title: "Holes for screws and nuts", text: "Round, rectangle, triangle or hexagon. M2 to M8 screw and nut sizes ready, with a little room added so the print actually fits." },
{ icon: "▤", title: "Copies and thickness", text: "Copies in a row or round in a circle. Make a surface thick, or a solid hollow with walls exactly as thick as you need." }, { icon: "▤", title: "Copies and thickness", text: "Copies in a row or round in a circle. Make a surface thick, or a solid hollow with walls exactly as thick as you need." },
{ icon: "🖌", title: "Paint and stickers", text: "Paint on the model or on its flat picture. Paste any picture (Ctrl+V) and it goes on like a sticker that bends over edges and corners." }, { icon: "✐", title: "Paint and stickers", text: "Paint on the model or on its flat picture. Paste any picture (Ctrl+V) and it goes on like a sticker that bends over edges and corners." },
{ icon: "▦", title: "Layout tab", text: "Move, turn and size the flat frames of the surface by hand. The paint follows, so the model looks the same." }, { icon: "▦", title: "Layout tab", text: "Move, turn and size the flat frames of the surface by hand. The paint follows, so the model looks the same." },
{ icon: "⎙", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." }, { icon: "⎙", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." },
{ icon: "☀", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." }, { icon: "☀", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." },
{ icon: "🗀", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." }, { icon: "▭", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." },
{ icon: "📱", title: "On your phone too", text: "Not a viewer: the whole modeller. Big buttons, one finger turns the view, two move and pinch to zoom, holding still is a right-click. Files through the phone's own picker." }, { icon: "▯", title: "On your phone too", text: "Not a viewer: the whole modeller. Big buttons, one finger turns the view, two move and pinch to zoom, holding still is a right-click. Files through the phone's own picker." },
{ icon: "💾", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." }, { icon: "⟲", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." },
], ],
shots: [ shots: [

92
docs/fonts/OFL-Inter.txt Normal file
View file

@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View file

@ -78,7 +78,7 @@
<footer> <footer>
<p>Fry3DX, made by <a id="author" href="#"></a>.</p> <p>Fry3DX, made by <a id="author" href="#"></a>.</p>
<p class="muted">Open source under the Apache License 2.0 · Sky pictures from Poly Haven (CC0).</p> <p class="muted">Open source under the Apache License 2.0 · Sky pictures from Poly Haven (CC0) · Fonts: Inter, JetBrains Mono (OFL).</p>
</footer> </footer>
<script src="content.js"></script> <script src="content.js"></script>

View file

@ -1,3 +1,6 @@
/* The site's own fonts (SIL Open Font License, fonts/OFL-*.txt), so it looks the same everywhere. */
@font-face { font-family: "Inter"; src: url("fonts/inter-latin-wght-normal.woff2") format("woff2"); font-weight: 100 900; font-display: swap; }
@font-face { font-family: "JetBrains Mono"; src: url("fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2"); font-weight: 100 800; font-display: swap; }
:root { :root {
--bg: #161618; --bg: #161618;
--panel: #1f1f23; --panel: #1f1f23;
@ -16,7 +19,7 @@ body {
margin: 0; margin: 0;
background: var(--bg); background: var(--bg);
color: var(--ink); color: var(--ink);
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font: 16px/1.6 "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
} }
a { color: var(--hot); text-decoration: none; } a { color: var(--hot); text-decoration: none; }
a:hover { text-decoration: underline; } a:hover { text-decoration: underline; }
@ -80,15 +83,15 @@ section:last-child { border-bottom: 0; }
.keys { border-collapse: collapse; width: 100%; } .keys { border-collapse: collapse; width: 100%; }
.keys td { padding: 7px 0; border-bottom: 1px solid var(--line); } .keys td { padding: 7px 0; border-bottom: 1px solid var(--line); }
.keys td:first-child { width: 45%; } .keys td:first-child { width: 45%; }
kbd { font: .85rem ui-monospace, "DejaVu Sans Mono", monospace; background: var(--panel2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 2px 7px; } kbd { font: .85rem "JetBrains Mono", ui-monospace, Consolas, monospace; background: var(--panel2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 2px 7px; }
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 28px; } .downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 28px; }
.download { display: flex; flex-direction: column; gap: 4px; padding: 18px 20px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); color: var(--ink); } .download { display: flex; flex-direction: column; gap: 4px; padding: 18px 20px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); color: var(--ink); }
.download:hover { text-decoration: none; border-color: var(--hot); } .download:hover { text-decoration: none; border-color: var(--hot); }
.download strong { font-size: 1.15rem; color: var(--hot); } .download strong { font-size: 1.15rem; color: var(--hot); }
.download .file { font: .85rem ui-monospace, "DejaVu Sans Mono", monospace; color: #cfcdc7; } .download .file { font: .85rem "JetBrains Mono", ui-monospace, Consolas, monospace; color: #cfcdc7; }
.code { position: relative; background: #0f0f11; border: 1px solid var(--line); border-left: 3px solid var(--hot); border-radius: var(--radius); } .code { position: relative; background: #0f0f11; border: 1px solid var(--line); border-left: 3px solid var(--hot); border-radius: var(--radius); }
.code pre { margin: 0; padding: 18px 20px; overflow-x: auto; font: .92rem/1.7 ui-monospace, "DejaVu Sans Mono", monospace; color: #d6d4cd; } .code pre { margin: 0; padding: 18px 20px; overflow-x: auto; font: .92rem/1.7 "JetBrains Mono", ui-monospace, Consolas, monospace; color: #d6d4cd; }
.copy { position: absolute; top: 10px; right: 10px; font: inherit; font-size: .85rem; color: var(--ink); background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 4px 12px; cursor: pointer; } .copy { position: absolute; top: 10px; right: 10px; font: inherit; font-size: .85rem; color: var(--ink); background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 4px 12px; cursor: pointer; }
.copy:hover { border-color: var(--hot); } .copy:hover { border-color: var(--hot); }

View file

@ -16,6 +16,9 @@ add_custom_target(keel_version COMMAND ${CMAKE_COMMAND} -DSRC=${PROJECT_SOURCE_D
add_dependencies(keel keel_version) add_dependencies(keel keel_version)
target_include_directories(keel PRIVATE ${CMAKE_BINARY_DIR}/generated) target_include_directories(keel PRIVATE ${CMAKE_BINARY_DIR}/generated)
target_link_libraries(keel PRIVATE stb miniaudio opusfile) target_link_libraries(keel PRIVATE stb miniaudio opusfile)
if(ANDROID)
target_link_libraries(keel PUBLIC log) # logcat
endif()
target_link_libraries(keel PUBLIC zstd_seekable glm::glm yapf SDL3::SDL3 volk vk-bootstrap::vk-bootstrap GPUOpen::VulkanMemoryAllocator imgui) target_link_libraries(keel PUBLIC zstd_seekable glm::glm yapf SDL3::SDL3 volk vk-bootstrap::vk-bootstrap GPUOpen::VulkanMemoryAllocator imgui)
target_compile_definitions(keel PUBLIC GLM_FORCE_DEPTH_ZERO_TO_ONE GLM_ENABLE_EXPERIMENTAL VK_NO_PROTOTYPES) target_compile_definitions(keel PUBLIC GLM_FORCE_DEPTH_ZERO_TO_ONE GLM_ENABLE_EXPERIMENTAL VK_NO_PROTOTYPES)
keel_add_shaders(keel shaders/box.vert shaders/box.frag shaders/mesh.vert shaders/mesh.frag shaders/depth.frag shaders/cull.comp shaders/froxel.comp shaders/froxel_rt.comp shaders/gi.comp shaders/skinned.vert keel_add_shaders(keel shaders/box.vert shaders/box.frag shaders/mesh.vert shaders/mesh.frag shaders/depth.frag shaders/cull.comp shaders/froxel.comp shaders/froxel_rt.comp shaders/gi.comp shaders/skinned.vert

View file

@ -60,7 +60,7 @@ Cvar vid_mode("vid_mode", "0", CVAR_ARCHIVE, "0 = window, 1 = borderless fullscr
Cvar vid_width("vid_width", "1600", CVAR_ARCHIVE, "Window / fullscreen width"); Cvar vid_width("vid_width", "1600", CVAR_ARCHIVE, "Window / fullscreen width");
Cvar vid_height("vid_height", "900", CVAR_ARCHIVE, "Window / fullscreen height"); Cvar vid_height("vid_height", "900", CVAR_ARCHIVE, "Window / fullscreen height");
Cvar r_renderer("r_renderer", "vulkan", CVAR_ARCHIVE, Cvar r_renderer("r_renderer", "vulkan", CVAR_ARCHIVE,
"Graphics API: vulkan, opengl (4.6), or the cut-down fallbacks for old GPUs: opengl40, opengl31 (restart needed)"); "Graphics API: vulkan, opengl (4.6), or the cut-down fallbacks for old GPUs: opengl40, opengl31, gles (OpenGL ES 3.0) (restart needed)");
Cvar hotreload("hotreload", "1", CVAR_ARCHIVE, Cvar hotreload("hotreload", "1", CVAR_ARCHIVE,
"Reload textures, models, sounds, scripts and the map when their files change (with tools/watch.py compiling as you save)"); "Reload textures, models, sounds, scripts and the map when their files change (with tools/watch.py compiling as you save)");
Cvar fps_max_menu("fps_max_menu", "120", CVAR_ARCHIVE, "Frame rate limit in menus and while paused (0 = same as fps_max)"); Cvar fps_max_menu("fps_max_menu", "120", CVAR_ARCHIVE, "Frame rate limit in menus and while paused (0 = same as fps_max)");
@ -283,12 +283,25 @@ int runClient(Game& game) {
const int w = std::max(vid_width.i(), 640), h = std::max(vid_height.i(), 480); const int w = std::max(vid_width.i(), 640), h = std::max(vid_height.i(), 480);
// The API asked for, then the others: the modern pair, then the fallbacks for old GPUs. // The API asked for, then the others: the modern pair, then the fallbacks for old GPUs.
const std::string want = r_renderer.str(); const std::string want = r_renderer.str();
const RenderApi first = want == "opengl" ? RenderApi::OpenGL : want == "opengl40" ? RenderApi::OpenGL40 : want == "opengl31" ? RenderApi::OpenGL31 : RenderApi::Vulkan; // (A phone has only Vulkan and OpenGL ES.)
RenderApi first = want == "opengl" ? RenderApi::OpenGL : want == "opengl40" ? RenderApi::OpenGL40 : want == "opengl31" ? RenderApi::OpenGL31
: want == "gles" ? RenderApi::GLES30 : RenderApi::Vulkan;
#if defined(__ANDROID__)
if (first != RenderApi::Vulkan) first = RenderApi::GLES30;
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::GLES30};
#else
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::OpenGL, RenderApi::OpenGL40, RenderApi::OpenGL31, RenderApi::GLES30};
#endif
std::vector<RenderApi> order{first}; std::vector<RenderApi> order{first};
for (RenderApi a : {RenderApi::Vulkan, RenderApi::OpenGL, RenderApi::OpenGL40, RenderApi::OpenGL31}) for (RenderApi a : all)
if (std::find(order.begin(), order.end(), a) == order.end() && (first != RenderApi::OpenGL31 || a == RenderApi::OpenGL31)) order.push_back(a); if (std::find(order.begin(), order.end(), a) == order.end() && (first != RenderApi::OpenGL31 || a == RenderApi::OpenGL31 || a == RenderApi::GLES30)) order.push_back(a);
bool ok = false; bool ok = false;
for (RenderApi api : order) { for (RenderApi api : order) {
if (api == RenderApi::GLES30) { // (before the window: a phone picks its surface's kind when it's made)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
}
ok = window.init(game.title(), w, h, api != RenderApi::Vulkan) && renderer.init(window, r_validation.b(), api); ok = window.init(game.title(), w, h, api != RenderApi::Vulkan) && renderer.init(window, r_validation.b(), api);
if (ok) { if (ok) {
if (api != first) if (api != first)

View file

@ -19,8 +19,8 @@ namespace keel {
class Window; class Window;
using TextureId = uint32_t; using TextureId = uint32_t;
// OpenGL40/OpenGL31: the cut-down fallback for old GPUs (rhi/gl/gl_legacy.cpp). // OpenGL40/OpenGL31/GLES30: the cut-down fallback for old GPUs and phones (rhi/gl/gl_legacy.cpp).
enum class RenderApi { Vulkan, OpenGL, OpenGL40, OpenGL31 }; enum class RenderApi { Vulkan, OpenGL, OpenGL40, OpenGL31, GLES30 };
// Texture slots. 0 and 1 are created by the renderer; 2-4 are the backend's render targets // Texture slots. 0 and 1 are created by the renderer; 2-4 are the backend's render targets
// (sampled through the same table); loaded textures start at kFirstTextureSlot. // (sampled through the same table); loaded textures start at kFirstTextureSlot.
@ -235,6 +235,6 @@ public:
std::unique_ptr<RenderBackend> createVulkanBackend(); std::unique_ptr<RenderBackend> createVulkanBackend();
std::unique_ptr<RenderBackend> createOpenGLBackend(); std::unique_ptr<RenderBackend> createOpenGLBackend();
std::unique_ptr<RenderBackend> createLegacyOpenGLBackend(int major, int minor); std::unique_ptr<RenderBackend> createLegacyOpenGLBackend(int major, int minor, bool es = false);
} // namespace keel } // namespace keel

View file

@ -114,6 +114,7 @@ bool Renderer::init(const Window& window, bool validation, RenderApi api) {
case RenderApi::OpenGL: backend_ = createOpenGLBackend(); break; case RenderApi::OpenGL: backend_ = createOpenGLBackend(); break;
case RenderApi::OpenGL40: backend_ = createLegacyOpenGLBackend(4, 0); break; case RenderApi::OpenGL40: backend_ = createLegacyOpenGLBackend(4, 0); break;
case RenderApi::OpenGL31: backend_ = createLegacyOpenGLBackend(3, 1); break; case RenderApi::OpenGL31: backend_ = createLegacyOpenGLBackend(3, 1); break;
case RenderApi::GLES30: backend_ = createLegacyOpenGLBackend(3, 0, true); break;
default: backend_ = createVulkanBackend(); break; default: backend_ = createVulkanBackend(); break;
} }
if (!backend_->init(window, validation)) { if (!backend_->init(window, validation)) {

View file

@ -1,4 +1,5 @@
// The fallback renderer for old GPUs: OpenGL 4.0 or 3.1, GLSL 1.40, nothing newer — bound // The fallback renderer for old GPUs: OpenGL 4.0 or 3.1 (GLSL 1.40), or OpenGL ES 3.0 for phones
// without Vulkan (GLSL ES 3.00: no skinning, as it has no texture buffers), nothing newer — bound
// textures, vertex attributes, one uniform block for the frame, a texture buffer for skinning. // textures, vertex attributes, one uniform block for the frame, a texture buffer for skinning.
// Two tiers, cut down from the main renderer: // Two tiers, cut down from the main renderer:
// 4.0 lightmaps, sun with cascaded shadows (PCF), HDR sky, fog, normal maps, GGX highlights, // 4.0 lightmaps, sun with cascaded shadows (PCF), HDR sky, fog, normal maps, GGX highlights,
@ -38,13 +39,11 @@ namespace {
X(void, Viewport, (GLint, GLint, GLsizei, GLsizei)) \ X(void, Viewport, (GLint, GLint, GLsizei, GLsizei)) \
X(void, Clear, (GLbitfield)) \ X(void, Clear, (GLbitfield)) \
X(void, ClearColor, (GLfloat, GLfloat, GLfloat, GLfloat)) \ X(void, ClearColor, (GLfloat, GLfloat, GLfloat, GLfloat)) \
X(void, ClearDepth, (GLdouble)) \
X(void, DepthFunc, (GLenum)) \ X(void, DepthFunc, (GLenum)) \
X(void, DepthMask, (GLboolean)) \ X(void, DepthMask, (GLboolean)) \
X(void, BlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum)) \ X(void, BlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum)) \
X(void, PolygonOffset, (GLfloat, GLfloat)) \ X(void, PolygonOffset, (GLfloat, GLfloat)) \
X(void, PixelStorei, (GLenum, GLint)) \ X(void, PixelStorei, (GLenum, GLint)) \
X(void, DrawBuffer, (GLenum)) \
X(void, ReadBuffer, (GLenum)) \ X(void, ReadBuffer, (GLenum)) \
X(void, ReadPixels, (GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*)) \ X(void, ReadPixels, (GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*)) \
X(void, GenBuffers, (GLsizei, GLuint*)) \ X(void, GenBuffers, (GLsizei, GLuint*)) \
@ -59,7 +58,6 @@ namespace {
X(void, ActiveTexture, (GLenum)) \ X(void, ActiveTexture, (GLenum)) \
X(void, TexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*)) \ X(void, TexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*)) \
X(void, TexParameteri, (GLenum, GLenum, GLint)) \ X(void, TexParameteri, (GLenum, GLenum, GLint)) \
X(void, TexBuffer, (GLenum, GLenum, GLuint)) \
X(void, GenFramebuffers, (GLsizei, GLuint*)) \ X(void, GenFramebuffers, (GLsizei, GLuint*)) \
X(void, DeleteFramebuffers, (GLsizei, const GLuint*)) \ X(void, DeleteFramebuffers, (GLsizei, const GLuint*)) \
X(void, BindFramebuffer, (GLenum, GLuint)) \ X(void, BindFramebuffer, (GLenum, GLuint)) \
@ -84,7 +82,6 @@ namespace {
X(GLuint, CreateProgram, (void)) \ X(GLuint, CreateProgram, (void)) \
X(void, AttachShader, (GLuint, GLuint)) \ X(void, AttachShader, (GLuint, GLuint)) \
X(void, BindAttribLocation, (GLuint, GLuint, const GLchar*)) \ X(void, BindAttribLocation, (GLuint, GLuint, const GLchar*)) \
X(void, BindFragDataLocation, (GLuint, GLuint, const GLchar*)) \
X(void, LinkProgram, (GLuint)) \ X(void, LinkProgram, (GLuint)) \
X(void, GetProgramiv, (GLuint, GLenum, GLint*)) \ X(void, GetProgramiv, (GLuint, GLenum, GLint*)) \
X(void, GetProgramInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \ X(void, GetProgramInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
@ -96,10 +93,19 @@ namespace {
X(void, Uniform1i, (GLint, GLint)) \ X(void, Uniform1i, (GLint, GLint)) \
X(void, Uniform4fv, (GLint, GLsizei, const GLfloat*)) \ X(void, Uniform4fv, (GLint, GLsizei, const GLfloat*)) \
X(void, UniformMatrix4fv, (GLint, GLsizei, GLboolean, const GLfloat*)) X(void, UniformMatrix4fv, (GLint, GLsizei, GLboolean, const GLfloat*))
// Desktop GL only, or ES only: whichever there is.
#define KEEL_GLX_OPTIONAL(X) \
X(void, ClearDepth, (GLdouble)) \
X(void, ClearDepthf, (GLfloat)) \
X(void, DrawBuffer, (GLenum)) \
X(void, DrawBuffers, (GLsizei, const GLenum*)) \
X(void, TexBuffer, (GLenum, GLenum, GLuint)) \
X(void, BindFragDataLocation, (GLuint, GLuint, const GLchar*))
namespace glx { namespace glx {
#define KEEL_GLX_DECLARE(ret, name, params) using name##_t = ret(APIENTRY*) params; name##_t name = nullptr; #define KEEL_GLX_DECLARE(ret, name, params) using name##_t = ret(APIENTRY*) params; name##_t name = nullptr;
KEEL_GLX_FUNCTIONS(KEEL_GLX_DECLARE) KEEL_GLX_FUNCTIONS(KEEL_GLX_DECLARE)
KEEL_GLX_OPTIONAL(KEEL_GLX_DECLARE)
#undef KEEL_GLX_DECLARE #undef KEEL_GLX_DECLARE
bool load() { bool load() {
std::string missing; std::string missing;
@ -108,12 +114,15 @@ bool load() {
if (!name) missing += " gl" #name; if (!name) missing += " gl" #name;
KEEL_GLX_FUNCTIONS(KEEL_GLX_LOAD) KEEL_GLX_FUNCTIONS(KEEL_GLX_LOAD)
#undef KEEL_GLX_LOAD #undef KEEL_GLX_LOAD
#define KEEL_GLX_LOAD_OPTIONAL(ret, name, params) name = reinterpret_cast<name##_t>(SDL_GL_GetProcAddress("gl" #name));
KEEL_GLX_OPTIONAL(KEEL_GLX_LOAD_OPTIONAL)
#undef KEEL_GLX_LOAD_OPTIONAL
if (!missing.empty()) log::warn("OpenGL (legacy): missing functions:{}", missing); if (!missing.empty()) log::warn("OpenGL (legacy): missing functions:{}", missing);
return missing.empty(); return missing.empty();
} }
} // namespace glx } // namespace glx
// ── Shaders (GLSL 1.40; TIER is 1 for GL 3.1, 2 for 4.0) ── // ── Shaders (GLSL 1.40 or ES 3.00; TIER is 1 for GL 3.1, 2 for 4.0 and ES 3.0) ──
const char* kFrameBlock = R"( const char* kFrameBlock = R"(
layout(std140) uniform Frame { // (GLSL 1.40: no instance name, so the members say F_) layout(std140) uniform Frame { // (GLSL 1.40: no instance name, so the members say F_)
mat4 F_cascades[4]; mat4 F_cascades[4];
@ -126,12 +135,16 @@ const char* kMeshVert = R"(
in vec3 a_pos; in vec3 a_normal; in vec2 a_uv; in vec2 a_lmuv; in uvec4 a_joints; in vec4 a_weights; in vec3 a_pos; in vec3 a_normal; in vec2 a_uv; in vec2 a_lmuv; in uvec4 a_joints; in vec4 a_weights;
uniform mat4 u_viewProj, u_model; uniform mat4 u_viewProj, u_model;
uniform int u_skinned, u_firstJoint; uniform int u_skinned, u_firstJoint;
uniform samplerBuffer u_joints;
out vec3 v_normal, v_world; out vec2 v_uv, v_lmuv; out vec3 v_normal, v_world; out vec2 v_uv, v_lmuv;
#ifdef GL_ES
mat4 joint(uint j) { return u_model; } // (never skinned here)
#else
uniform samplerBuffer u_joints;
mat4 joint(uint j) { mat4 joint(uint j) {
int b = (u_firstJoint + int(j)) * 4; int b = (u_firstJoint + int(j)) * 4;
return mat4(texelFetch(u_joints, b), texelFetch(u_joints, b + 1), texelFetch(u_joints, b + 2), texelFetch(u_joints, b + 3)); return mat4(texelFetch(u_joints, b), texelFetch(u_joints, b + 1), texelFetch(u_joints, b + 2), texelFetch(u_joints, b + 3));
} }
#endif
void main() { void main() {
mat4 m = u_model; mat4 m = u_model;
if (u_skinned != 0) if (u_skinned != 0)
@ -345,6 +358,33 @@ out vec4 o_color;
void main() { o_color = vec4(u_color.rgb * (0.5 + 0.5 * max(dot(v_normal, normalize(vec3(0.4, 0.3, 0.85))), 0.0)), u_color.a); } void main() { o_color = vec4(u_color.rgb * (0.5 + 0.5 * max(dot(v_normal, normalize(vec3(0.4, 0.3, 0.85))), 0.0)), u_color.a); }
)"; )";
// Sprites and streaks (the grid, the axes, particles): a quad facing the camera, textured, unlit.
const char* kSpriteVert = R"(
uniform mat4 u_viewProj;
uniform vec4 u_posSize, u_right, u_up; // right.w: turn (radians)
out vec2 v_uv;
const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1));
void main() {
vec2 q = kQuad[gl_VertexID];
float c = cos(u_right.w), s = sin(u_right.w);
vec2 r = vec2(c * q.x - s * q.y, s * q.x + c * q.y);
vec3 world = u_posSize.xyz + (u_right.xyz * r.x + u_up.xyz * r.y) * u_posSize.w;
v_uv = vec2(q.x * 0.5 + 0.5, 0.5 - q.y * 0.5);
gl_Position = u_viewProj * vec4(world, 1.0);
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w;
}
)";
const char* kSpriteFrag = R"(
in vec2 v_uv;
uniform sampler2D u_albedo;
uniform vec4 u_color;
out vec4 o_color;
void main() {
vec4 t = texture(u_albedo, v_uv);
o_color = vec4(t.rgb * u_color.rgb, clamp((t.a - 0.03) / 0.97, 0.0, 1.0) * u_color.a);
}
)";
const char* kTonemapVert = R"( const char* kTonemapVert = R"(
out vec2 v_uv; out vec2 v_uv;
void main() { void main() {
@ -376,13 +416,13 @@ constexpr int kMaxLegacyLights = 16;
struct Program { struct Program {
GLuint id = 0; GLuint id = 0;
GLint viewProj = -1, model = -1, skinned = -1, firstJoint = -1, tint = -1, params = -1, plane = -1, flags = -1, cube = -1, GLint viewProj = -1, model = -1, skinned = -1, firstJoint = -1, tint = -1, params = -1, plane = -1, flags = -1, cube = -1,
invViewProj = -1, center = -1, extents = -1, color = -1, exposure = -1; invViewProj = -1, center = -1, extents = -1, color = -1, exposure = -1, posSize = -1, right = -1, up = -1;
}; };
class LegacyGLBackend final : public RenderBackend { class LegacyGLBackend final : public RenderBackend {
public: public:
explicit LegacyGLBackend(int major, int minor) : major_(major), minor_(minor), tier_(major >= 4 ? 2 : 1) { LegacyGLBackend(int major, int minor, bool es) : major_(major), minor_(minor), tier_(major >= 4 || es ? 2 : 1), es_(es) {
name_ = "OpenGL " + std::to_string(major) + "." + std::to_string(minor) + " (fallback)"; name_ = std::string(es ? "OpenGL ES " : "OpenGL ") + std::to_string(major) + "." + std::to_string(minor) + " (fallback)";
} }
const char* name() const override { return name_.c_str(); } const char* name() const override { return name_.c_str(); }
GpuInfo gpuInfo() const override { return gpu_; } GpuInfo gpuInfo() const override { return gpu_; }
@ -393,8 +433,8 @@ public:
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, major_); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, major_);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minor_); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minor_);
// 3.1 has no profiles; 4.0 asks for core. // 3.1 has no profiles; 4.0 asks for core.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, major_ >= 4 ? SDL_GL_CONTEXT_PROFILE_CORE : 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, es_ ? SDL_GL_CONTEXT_PROFILE_ES : major_ >= 4 ? SDL_GL_CONTEXT_PROFILE_CORE : 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, major_ >= 4 ? 0 : SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, major_ >= 4 || es_ ? 0 : SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
context_ = SDL_GL_CreateContext(window_); context_ = SDL_GL_CreateContext(window_);
@ -409,23 +449,29 @@ public:
glx::PixelStorei(GL_UNPACK_ALIGNMENT, 1); glx::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
glx::PixelStorei(GL_PACK_ALIGNMENT, 1); glx::PixelStorei(GL_PACK_ALIGNMENT, 1);
const std::string header = "#version 140\n#define TIER " + std::to_string(tier_) + "\n" + kFrameBlock; const std::string header = std::string(es_ ? "#version 300 es\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\n"
"precision highp sampler2DShadow;\n"
: "#version 140\n") +
"#define TIER " + std::to_string(tier_) + "\n" + kFrameBlock;
mesh_ = link(header + kMeshVert, header + kMeshFrag); mesh_ = link(header + kMeshVert, header + kMeshFrag);
depth_ = link(header + kMeshVert, header + kDepthFrag); depth_ = link(header + kMeshVert, header + kDepthFrag);
sky_ = link(header + kSkyVert, header + kSkyFrag); sky_ = link(header + kSkyVert, header + kSkyFrag);
box_ = link(header + kBoxVert, header + kBoxFrag); box_ = link(header + kBoxVert, header + kBoxFrag);
tonemap_ = link(header + kTonemapVert, header + kTonemapFrag); tonemap_ = link(header + kTonemapVert, header + kTonemapFrag);
if (!mesh_.id || !depth_.id || !sky_.id || !box_.id || !tonemap_.id) return false; sprite_ = link(header + kSpriteVert, header + kSpriteFrag);
if (!mesh_.id || !depth_.id || !sky_.id || !box_.id || !tonemap_.id || !sprite_.id) return false;
glx::GenBuffers(1, &frameUbo_); glx::GenBuffers(1, &frameUbo_);
glx::BindBuffer(GL_UNIFORM_BUFFER, frameUbo_); glx::BindBuffer(GL_UNIFORM_BUFFER, frameUbo_);
glx::BufferData(GL_UNIFORM_BUFFER, sizeof(FrameBlock), nullptr, GL_DYNAMIC_DRAW); glx::BufferData(GL_UNIFORM_BUFFER, sizeof(FrameBlock), nullptr, GL_DYNAMIC_DRAW);
glx::GenBuffers(1, &jointBuffer_); if (glx::TexBuffer && !es_) {
glx::BindBuffer(GL_TEXTURE_BUFFER, jointBuffer_); glx::GenBuffers(1, &jointBuffer_);
glx::BufferData(GL_TEXTURE_BUFFER, kMaxJointsPerFrame * sizeof(glm::mat4), nullptr, GL_DYNAMIC_DRAW); glx::BindBuffer(GL_TEXTURE_BUFFER, jointBuffer_);
glx::GenTextures(1, &jointTexture_); glx::BufferData(GL_TEXTURE_BUFFER, kMaxJointsPerFrame * sizeof(glm::mat4), nullptr, GL_DYNAMIC_DRAW);
glx::BindTexture(GL_TEXTURE_BUFFER, jointTexture_); glx::GenTextures(1, &jointTexture_);
glx::TexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, jointBuffer_); glx::BindTexture(GL_TEXTURE_BUFFER, jointTexture_);
glx::TexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, jointBuffer_);
}
glx::GenVertexArrays(1, &emptyVao_); glx::GenVertexArrays(1, &emptyVao_);
// The sun's shadow map: 2×2 cascades, smaller on the 3.1 tier. // The sun's shadow map: 2×2 cascades, smaller on the 3.1 tier.
@ -442,7 +488,9 @@ public:
glx::GenFramebuffers(1, &shadowFbo_); glx::GenFramebuffers(1, &shadowFbo_);
glx::BindFramebuffer(GL_FRAMEBUFFER, shadowFbo_); glx::BindFramebuffer(GL_FRAMEBUFFER, shadowFbo_);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowMap_, 0); glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowMap_, 0);
glx::DrawBuffer(GL_NONE); const GLenum none = GL_NONE;
if (glx::DrawBuffer) glx::DrawBuffer(GL_NONE);
else if (glx::DrawBuffers) glx::DrawBuffers(1, &none);
glx::ReadBuffer(GL_NONE); glx::ReadBuffer(GL_NONE);
glx::BindFramebuffer(GL_FRAMEBUFFER, 0); glx::BindFramebuffer(GL_FRAMEBUFFER, 0);
@ -450,7 +498,7 @@ public:
ImGui::CreateContext(); ImGui::CreateContext();
ImGui::GetIO().IniFilename = nullptr; ImGui::GetIO().IniFilename = nullptr;
ImGui::StyleColorsDark(); ImGui::StyleColorsDark();
imgui_ = ImGui_ImplSDL3_InitForOpenGL(window_, context_) && ImGui_ImplOpenGL3_Init("#version 140"); imgui_ = ImGui_ImplSDL3_InitForOpenGL(window_, context_) && ImGui_ImplOpenGL3_Init(es_ ? "#version 300 es" : "#version 140");
log::info("Fallback renderer: tier {} ({})", tier_, tier_ >= 2 ? "shadows, normal maps, highlights, sky reflections, 16 lights" : "lightmaps, sun shadows, sky"); log::info("Fallback renderer: tier {} ({})", tier_, tier_ >= 2 ? "shadows, normal maps, highlights, sky reflections, 16 lights" : "lightmaps, sun shadows, sky");
return imgui_; return imgui_;
} }
@ -584,7 +632,7 @@ private:
glx::AttachShader(p.id, f); glx::AttachShader(p.id, f);
const char* attrs[] = {"a_pos", "a_normal", "a_uv", "a_lmuv", "a_joints", "a_weights"}; const char* attrs[] = {"a_pos", "a_normal", "a_uv", "a_lmuv", "a_joints", "a_weights"};
for (GLuint i = 0; i < 6; ++i) glx::BindAttribLocation(p.id, i, attrs[i]); for (GLuint i = 0; i < 6; ++i) glx::BindAttribLocation(p.id, i, attrs[i]);
glx::BindFragDataLocation(p.id, 0, "o_color"); if (glx::BindFragDataLocation) glx::BindFragDataLocation(p.id, 0, "o_color"); // (ES: the only output is 0)
glx::LinkProgram(p.id); glx::LinkProgram(p.id);
glx::DeleteShader(v); glx::DeleteShader(v);
glx::DeleteShader(f); glx::DeleteShader(f);
@ -603,6 +651,7 @@ private:
p.viewProj = u("u_viewProj"), p.model = u("u_model"), p.skinned = u("u_skinned"), p.firstJoint = u("u_firstJoint"); p.viewProj = u("u_viewProj"), p.model = u("u_model"), p.skinned = u("u_skinned"), p.firstJoint = u("u_firstJoint");
p.tint = u("u_tint"), p.params = u("u_params"), p.plane = u("u_plane"), p.flags = u("u_flags"), p.cube = u("u_cube"); p.tint = u("u_tint"), p.params = u("u_params"), p.plane = u("u_plane"), p.flags = u("u_flags"), p.cube = u("u_cube");
p.invViewProj = u("u_invViewProj"), p.center = u("u_center"), p.extents = u("u_extents"), p.color = u("u_color"), p.exposure = u("u_exposure"); p.invViewProj = u("u_invViewProj"), p.center = u("u_center"), p.extents = u("u_extents"), p.color = u("u_color"), p.exposure = u("u_exposure");
p.posSize = u("u_posSize"), p.right = u("u_right"), p.up = u("u_up");
// Fixed texture units. // Fixed texture units.
glx::UseProgram(p.id); glx::UseProgram(p.id);
const struct { const char* name; GLint unit; } units[] = {{"u_albedo", 0}, {"u_lightmap", 1}, {"u_material", 2}, {"u_skyVis", 3}, const struct { const char* name; GLint unit; } units[] = {{"u_albedo", 0}, {"u_lightmap", 1}, {"u_material", 2}, {"u_skyVis", 3},
@ -628,13 +677,18 @@ private:
void ensureTargets(uint32_t w, uint32_t h) { void ensureTargets(uint32_t w, uint32_t h) {
if (sceneFbo_ && w == targetW_ && h == targetH_) return; if (sceneFbo_ && w == targetW_ && h == targetH_) return;
releaseTargets(); releaseTargets();
glx::GenTextures(1, &sceneTex_); // HDR, or (a phone that can't draw into half floats) plain 8 bits: brights clip.
glx::BindTexture(GL_TEXTURE_2D, sceneTex_); auto colour = [&](bool hdr) {
glx::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, GLsizei(w), GLsizei(h), 0, GL_RGBA, GL_FLOAT, nullptr); if (sceneTex_) glx::DeleteTextures(1, &sceneTex_);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glx::GenTextures(1, &sceneTex_);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glx::BindTexture(GL_TEXTURE_2D, sceneTex_);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glx::TexImage2D(GL_TEXTURE_2D, 0, hdr ? GL_RGBA16F : GL_RGBA8, GLsizei(w), GLsizei(h), 0, GL_RGBA, hdr ? GL_HALF_FLOAT : GL_UNSIGNED_BYTE, nullptr);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
};
colour(!ldrTarget_);
glx::GenTextures(1, &sceneDepth_); glx::GenTextures(1, &sceneDepth_);
glx::BindTexture(GL_TEXTURE_2D, sceneDepth_); glx::BindTexture(GL_TEXTURE_2D, sceneDepth_);
glx::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, GLsizei(w), GLsizei(h), 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr); glx::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, GLsizei(w), GLsizei(h), 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr);
@ -644,6 +698,12 @@ private:
glx::BindFramebuffer(GL_FRAMEBUFFER, sceneFbo_); glx::BindFramebuffer(GL_FRAMEBUFFER, sceneFbo_);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sceneTex_, 0); glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sceneTex_, 0);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, sceneDepth_, 0); glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, sceneDepth_, 0);
if (!ldrTarget_ && glx::CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
log::warn("OpenGL (legacy): can't draw into half floats; the scene is 8 bits");
ldrTarget_ = true;
colour(false);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sceneTex_, 0);
}
if (glx::CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) log::error("OpenGL (legacy): scene framebuffer incomplete"); if (glx::CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) log::error("OpenGL (legacy): scene framebuffer incomplete");
glx::BindFramebuffer(GL_FRAMEBUFFER, 0); glx::BindFramebuffer(GL_FRAMEBUFFER, 0);
targetW_ = w, targetH_ = h; targetW_ = w, targetH_ = h;
@ -656,10 +716,11 @@ private:
std::string name_; std::string name_;
int major_, minor_, tier_; int major_, minor_, tier_;
bool es_ = false, ldrTarget_ = false;
SDL_Window* window_ = nullptr; SDL_Window* window_ = nullptr;
SDL_GLContext context_ = nullptr; SDL_GLContext context_ = nullptr;
bool imgui_ = false; bool imgui_ = false;
Program mesh_, depth_, sky_, box_, tonemap_; Program mesh_, depth_, sky_, box_, tonemap_, sprite_;
std::vector<GLuint> textures_; std::vector<GLuint> textures_;
std::vector<Model> models_; std::vector<Model> models_;
GLuint frameUbo_ = 0, jointBuffer_ = 0, jointTexture_ = 0, emptyVao_ = 0; GLuint frameUbo_ = 0, jointBuffer_ = 0, jointTexture_ = 0, emptyVao_ = 0;
@ -724,12 +785,14 @@ void LegacyGLBackend::render(FramePacket& p) {
glx::BindBuffer(GL_UNIFORM_BUFFER, frameUbo_); glx::BindBuffer(GL_UNIFORM_BUFFER, frameUbo_);
glx::BufferSubData(GL_UNIFORM_BUFFER, 0, sizeof fb, &fb); glx::BufferSubData(GL_UNIFORM_BUFFER, 0, sizeof fb, &fb);
glx::BindBufferBase(GL_UNIFORM_BUFFER, 0, frameUbo_); glx::BindBufferBase(GL_UNIFORM_BUFFER, 0, frameUbo_);
if (!p.joints.empty()) { if (!p.joints.empty() && jointBuffer_) {
glx::BindBuffer(GL_TEXTURE_BUFFER, jointBuffer_); glx::BindBuffer(GL_TEXTURE_BUFFER, jointBuffer_);
glx::BufferSubData(GL_TEXTURE_BUFFER, 0, GLsizeiptr(std::min(p.joints.size(), size_t(kMaxJointsPerFrame)) * sizeof(glm::mat4)), p.joints.data()); glx::BufferSubData(GL_TEXTURE_BUFFER, 0, GLsizeiptr(std::min(p.joints.size(), size_t(kMaxJointsPerFrame)) * sizeof(glm::mat4)), p.joints.data());
} }
glx::ActiveTexture(GL_TEXTURE6); if (jointTexture_) {
glx::BindTexture(GL_TEXTURE_BUFFER, jointTexture_); glx::ActiveTexture(GL_TEXTURE6);
glx::BindTexture(GL_TEXTURE_BUFFER, jointTexture_);
}
glx::Disable(GL_CULL_FACE); glx::Disable(GL_CULL_FACE);
glx::Disable(GL_BLEND); glx::Disable(GL_BLEND);
glx::Enable(GL_DEPTH_TEST); glx::Enable(GL_DEPTH_TEST);
@ -739,7 +802,7 @@ void LegacyGLBackend::render(FramePacket& p) {
// ── Sun shadows (light shadows aren't drawn on this renderer) ── // ── Sun shadows (light shadows aren't drawn on this renderer) ──
glx::BindFramebuffer(GL_FRAMEBUFFER, shadowFbo_); glx::BindFramebuffer(GL_FRAMEBUFFER, shadowFbo_);
glx::Viewport(0, 0, GLsizei(shadowSize_), GLsizei(shadowSize_)); glx::Viewport(0, 0, GLsizei(shadowSize_), GLsizei(shadowSize_));
glx::ClearDepth(1.0); glx::ClearDepth ? glx::ClearDepth(1.0) : glx::ClearDepthf(1.0f);
glx::Clear(GL_DEPTH_BUFFER_BIT); glx::Clear(GL_DEPTH_BUFFER_BIT);
glx::Enable(GL_POLYGON_OFFSET_FILL); glx::Enable(GL_POLYGON_OFFSET_FILL);
glx::PolygonOffset(2.5f, 2.0f); glx::PolygonOffset(2.5f, 2.0f);
@ -789,6 +852,31 @@ void LegacyGLBackend::render(FramePacket& p) {
glx::DepthMask(GL_TRUE); glx::DepthMask(GL_TRUE);
glx::Disable(GL_BLEND); glx::Disable(GL_BLEND);
} }
if (!p.sprites.empty() || !p.glowSprites.empty()) { // depth-tested, not written; blended, then added on
glx::UseProgram(sprite_.id);
glx::UniformMatrix4fv(sprite_.viewProj, 1, GL_FALSE, glm::value_ptr(p.viewProj));
glx::BindVertexArray(emptyVao_);
glx::Enable(GL_BLEND);
glx::DepthMask(GL_FALSE);
auto draw = [&](const std::vector<gpu::Sprite>& sprites) {
for (const gpu::Sprite& sp : sprites) {
uint32_t tex = 0;
std::memcpy(&tex, &sp.up.w, 4); // (the texture's index, as the float's bits)
bindTexture(0, TextureId(tex));
glx::Uniform4fv(sprite_.posSize, 1, glm::value_ptr(sp.posSize));
glx::Uniform4fv(sprite_.color, 1, glm::value_ptr(sp.color));
glx::Uniform4fv(sprite_.right, 1, glm::value_ptr(sp.right));
glx::Uniform4fv(sprite_.up, 1, glm::value_ptr(sp.up));
glx::DrawArrays(GL_TRIANGLES, 0, 6);
}
};
glx::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
draw(p.sprites);
glx::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ZERO, GL_ONE);
draw(p.glowSprites);
glx::DepthMask(GL_TRUE);
glx::Disable(GL_BLEND);
}
// ── Tonemap to the window, then UI ── // ── Tonemap to the window, then UI ──
glx::BindFramebuffer(GL_FRAMEBUFFER, 0); glx::BindFramebuffer(GL_FRAMEBUFFER, 0);
@ -817,6 +905,6 @@ void LegacyGLBackend::render(FramePacket& p) {
} }
} // namespace } // namespace
std::unique_ptr<RenderBackend> createLegacyOpenGLBackend(int major, int minor) { return std::make_unique<LegacyGLBackend>(major, minor); } std::unique_ptr<RenderBackend> createLegacyOpenGLBackend(int major, int minor, bool es) { return std::make_unique<LegacyGLBackend>(major, minor, es); }
} // namespace keel } // namespace keel