Compare commits

..

32 Commits

Author SHA1 Message Date
burkkyy 0f0b0d1434 Merge pull request 'Adding in sandbox + Logger upgrade' (#80) from sandbox into main
Reviewed-on: #80
2026-10-06 01:20:21 -07:00
burkkyy 777e407d67 sandbox simplfy 2026-10-06 01:16:28 -07:00
burkkyy e32e5f911f sandbox copying current application 2026-10-05 02:17:32 -07:00
burkkyy 5b2d50a925 stubbing out conway game of life 2026-10-05 02:16:55 -07:00
burkkyy f6f4f8b7c9 fixes to buffer class 2026-10-05 02:16:37 -07:00
burkkyy b6098d4dc6 New logger 2026-10-04 23:14:17 -07:00
burkkyy b31c120691 bumping project cxx version to 23 2026-10-04 22:54:07 -07:00
burkkyy 3c3fcfdd6b removing fmt 2026-10-04 22:52:07 -07:00
burkkyy 9538add3ac Using version 1.12.0 of fmt in project 2026-10-04 22:43:09 -07:00
burkkyy 31089f3539 adding fmt third_party library + adding cloc support 2026-10-04 22:41:42 -07:00
burkkyy 6cc8bf4262 adding in sandbox 2026-10-04 01:03:47 -07:00
burkkyy c99c720e55 Merge pull request 'Adding 3D support' (#77) from dev/adding-3d-support into main
Reviewed-on: #77
2026-10-02 20:12:46 -07:00
burkkyy 6e48b0605c adding and integrating ImGuizmo 2026-10-02 11:36:33 -07:00
burkkyy 379589fe8c renaming viewport panel to scene panel 2026-09-30 22:10:08 -07:00
burkkyy b5b0eb9497 Huge changes for 3d 2026-09-30 18:53:16 -07:00
burkkyy cd2e80cfca Merge pull request '2D scene rendering + image loading' (#15) from issue-14/2d-scene-support-spritecomponent-2d-render-system into main
Reviewed-on: #15
2026-09-30 15:56:33 -07:00
burkkyy ab7c850121 Move barriers and frame submit to synchronization2
Image barriers now carry their own stage masks and are recorded with
pipelineBarrier2, so barriers flushed together no longer share the
union of their stages. AccessState uses the 64-bit stage and access
flags, and the frame submit uses submit2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:33 -07:00
burkkyy 1ba1442cc4 Require GPU-driven device features
Enable descriptor indexing (bindless), buffer device address, scalar
block layout, indirect draw count, shader draw parameters, timeline
semaphores and synchronization2 alongside dynamic rendering. A single
feature list drives both physical device selection and device creation.
The VMA allocator is created with buffer device address support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:33 -07:00
burkkyy 7d87503eba restructuing dev command 2026-09-30 02:42:45 -07:00
burkkyy 9f3ad5f372 Adding tracy command for ease of use 2026-09-30 02:22:55 -07:00
burkkyy 812d50127e Adding in tracy profiler 2026-09-30 02:04:59 -07:00
burkkyy 97ff85ea8a Added VMA 2026-09-30 01:31:59 -07:00
burkkyy fc70475538 renaming dir 2026-09-27 02:02:19 -07:00
burkkyy 0f9c7ffc20 view -> sceneView in app class 2026-09-26 02:18:12 -07:00
burkkyy 0b09aeb5d9 More changes 2026-09-26 02:03:11 -07:00
burkkyy 8a82df59ca cool viewport 2026-09-26 01:50:09 -07:00
burkkyy b24945ec9d Bunch of changes + fixes 2026-09-26 01:49:45 -07:00
burkkyy e05926ad74 simplified build 2026-09-25 23:12:04 -07:00
burkkyy 571472a204 adding screenshot of using engine editor as a showcase 2026-09-25 23:09:56 -07:00
burkkyy a21a842b5b Initial claude dump 2026-09-25 22:59:01 -07:00
burkkyy 5644782a1f Adding in simple alpha blending 2026-09-25 22:34:25 -07:00
burkkyy b44fe94d5c image view example 2026-09-25 22:33:30 -07:00
131 changed files with 4916 additions and 1227 deletions
+8
View File
@@ -0,0 +1,8 @@
third_party
build
__dump
traces
.vscode
.claude
.git
imgui.ini
+1 -2
View File
@@ -14,5 +14,4 @@ TODO
## Testing Instructions
1. Build engine via `bin/dev build && cd build/ && make -j4`
2. Run testbed via `./testbed`
1. Build engine and run editor via `bin/dev editor`
+7
View File
@@ -32,6 +32,13 @@ pids
*.pid.lock
imgui.ini
# Python bytecode (bin/dev and bin/dev_commands)
__pycache__/
# Tracy captures
traces/
*.tracy
# dotenv environment variables file
.envrc
.env
+6
View File
@@ -8,3 +8,9 @@
path = third_party/imgui
url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "third_party/tracy"]
path = third_party/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "third_party/ImGuizmo"]
path = third_party/ImGuizmo
url = https://github.com/CedricGuillemet/ImGuizmo.git
+2 -1
View File
@@ -5,7 +5,7 @@ set(NAME sophia)
project(${NAME} VERSION 0.0.0)
enable_language(CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -39,4 +39,5 @@ add_subdirectory(third_party)
add_subdirectory(engine)
add_subdirectory(editor)
add_subdirectory(testbed)
add_subdirectory(sandbox)
add_subdirectory(examples)
+2
View File
@@ -1,5 +1,7 @@
# sophia
![showcase](./media/showcase.png)
## Getting Started
COMING SOON
+27 -245
View File
@@ -2,13 +2,16 @@
import json
import os
import re
import shlex
import shutil
import struct
import subprocess
import sys
from helpers.gitea import GiteaCommand
from helpers.renderdoc import RenderDocCommand
from helpers.shaders import ShadersCommand
from helpers.tracy import TracyCommand
COMMAND_TO_METHOD = {
"branch-from": "branch_from",
"bash-completions": "bash_completions",
@@ -17,8 +20,7 @@ METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.it
WAIT_FOR_PROCESS = "wait_for_process"
class DevHelper:
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
PROJECT_NAME = "sophia"
DEFAULT_PRESET = "debug"
@@ -79,88 +81,28 @@ class DevHelper:
os.execv(binary, [binary, *args])
##
# Checks out a branch named after a Gitea issue URL.
# If the issue is from this project's repo, the branch name will be:
# issue-<issue_number>/<issue_title>
# If the issue is from another repo (presumably upstream), the branch name will be:
# <issue_owner>-issue-<issue_number>/<issue_title>
def branch_from(self, *args, **kwargs):
issue_url = args[0] if args else None
if issue_url is None:
raise ValueError("Must provide a Gitea issue URL.")
# Builds the debug preset (incrementally), then runs the raw Vulkan
# sandbox from build/debug. Any arguments are passed through.
# dev sandbox [args...]
def sandbox(self, *args, **kwargs):
preset = self.DEFAULT_PRESET
self._build_preset(preset, fresh=False)
base_url, issue_repo, issue_number = self._parse_issue_url(issue_url)
issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number)
branch_name = self._format_branch_name(issue_repo, issue_number, issue_title)
exists = subprocess.run(
["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"],
stdout=subprocess.DEVNULL,
).returncode == 0
flag = "" if exists else "-b "
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)
##
# Creates an issue on this project's Gitea repo.
# dev issue <title> [-b|--body <text>] [-l|--label <name>]... [-a|--assignee <user>]... [-o|--open]
# The body is read from stdin if --body is not given and stdin is piped.
# Gitea's API cannot add issues to a project (only the web UI can), so the
# issue URL is printed and --open opens it in the browser to do so.
def issue(self, *args, **kwargs):
title, body, labels, assignees, open_in_browser = self._parse_issue_args(args)
repo = self._origin_repo()
if repo is None:
raise RuntimeError("could not determine the Gitea repo from the origin remote")
payload = {"title": title, "body": body}
if labels:
label_ids = self._fetch_label_ids(repo)
unknown = [label for label in labels if label not in label_ids]
if unknown:
raise ValueError(f"Unknown label(s): {', '.join(unknown)} (available: {', '.join(sorted(label_ids))})")
payload["labels"] = [label_ids[label] for label in labels]
if assignees:
payload["assignees"] = assignees
issue = self._tea_api(f"repos/{repo}/issues", payload)
print(f"Created issue #{issue['number']}: {issue['title']}")
print(issue["html_url"])
print(f"Add it to the project from the issue sidebar: Projects > {self.PROJECT_NAME}")
if open_in_browser:
opener = shutil.which("xdg-open") or shutil.which("open")
if opener is None:
raise RuntimeError("xdg-open not found on PATH")
subprocess.Popen([opener, issue["html_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def renderdoc(self, *args, **kwargs):
qrenderdoc = self._find_qrenderdoc()
self._exec_in_root(qrenderdoc, replace=True)
def shaders(self, *args, **kwargs):
shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders")
if not os.path.isdir(shaders_dir):
raise ValueError(f"Shaders directory not found: {shaders_dir}")
glslc = self._find_glslc()
shader_paths = sorted(
os.path.join(root, filename)
for root, _dirs, files in os.walk(shaders_dir)
for filename in files
if filename.endswith(self.SHADER_EXTENSIONS)
)
if not shader_paths:
print(f"No shaders found in {shaders_dir}")
return
for shader_path in shader_paths:
self._generate_shader_header(glslc, shader_path)
preset_dir = os.path.join(self._project_root(), "build", preset)
binary = os.path.join(preset_dir, "bin", "sandbox")
print(f"Running: {shlex.join([binary, *args])}")
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
# Custom helpers
##
# Counts lines of code from the project root, skipping paths listed in
# .clocignore. Any arguments are passed through to cloc.
# dev cloc [args...]
def cloc(self, *args, **kwargs):
self._run_in_root(["cloc", "--exclude-list-file=.clocignore", *args, "."])
def ownit(self, *args, **kwargs):
file_or_directory = args[0] if args else None
if file_or_directory is None:
@@ -180,8 +122,8 @@ class DevHelper:
def _commands(self):
return sorted(
METHOD_TO_COMMAND.get(name, name)
for name in vars(DevHelper)
if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
for name in dir(DevCommand)
if not name.startswith("_") and name != "call" and callable(getattr(DevCommand, name))
)
def _parse_preset_args(self, args, command):
@@ -230,166 +172,6 @@ class DevHelper:
if result.returncode != 0:
sys.exit(result.returncode)
def _parse_issue_url(self, url):
match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
if match is None:
raise ValueError(f"Not a Gitea issue URL: {url}")
return match[1], f"{match[2]}/{match[3]}", match[4]
def _fetch_issue_title(self, base_url, repo, issue_number):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}"
print(f"Running: tea api {endpoint}")
result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True)
if result.returncode != 0:
raise RuntimeError(f"failed to fetch issue title from {endpoint}")
return json.loads(result.stdout)["title"]
def _parse_issue_args(self, args):
title = None
body = None
labels = []
assignees = []
open_in_browser = False
args = list(args)
while args:
arg = args.pop(0)
if arg in ("-b", "--body", "-l", "--label", "-a", "--assignee"):
if not args:
raise ValueError(f"{arg} requires a value")
value = args.pop(0)
if arg in ("-b", "--body"):
body = value
elif arg in ("-l", "--label"):
labels.append(value)
else:
assignees.append(value)
elif arg in ("-o", "--open"):
open_in_browser = True
elif arg.startswith("-"):
raise ValueError(f"Unknown option: {arg}")
elif title is None:
title = arg
else:
title = f"{title} {arg}"
if not title:
raise ValueError("Must provide an issue title. Usage: dev issue <title> [-b <body>] [-l <label>]... [-a <user>]... [-o]")
if body is None:
body = "" if sys.stdin.isatty() else sys.stdin.read()
return title, body, labels, assignees, open_in_browser
def _tea_api(self, endpoint, payload=None):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
command = ["tea", "api", endpoint]
if payload is not None:
command += ["-d", "@-"]
print(f"Running: {' '.join(command)}")
result = subprocess.run(
command,
input=json.dumps(payload) if payload is not None else None,
stdout=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"tea api request failed: {endpoint}")
return json.loads(result.stdout)
def _fetch_label_ids(self, repo):
labels = self._tea_api(f"repos/{repo}/labels?limit=100")
return {label["name"]: label["id"] for label in labels}
def _format_branch_name(self, issue_repo, issue_number, issue_title):
formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip())))
if issue_repo == self._origin_repo():
return f"issue-{issue_number}/{formatted_title}"
issue_owner = issue_repo.split("/")[0]
return f"{issue_owner}-issue-{issue_number}/{formatted_title}"
def _origin_repo(self):
result = subprocess.run(
["git", "-C", self._project_root(), "remote", "get-url", "origin"],
stdout=subprocess.PIPE,
text=True,
)
match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip())
return match[1] if match else None
def _find_glslc(self):
glslc = shutil.which("glslc")
if glslc is None:
raise RuntimeError(
"glslc not found on PATH (install the Vulkan SDK)"
)
return glslc
def _find_qrenderdoc(self):
qrenderdoc = shutil.which("qrenderdoc")
if qrenderdoc is None:
raise RuntimeError(
"qrenderdoc not found on PATH (install RenderDoc)"
)
return qrenderdoc
def _generate_shader_header(self, glslc, shader_path):
relative_path = os.path.relpath(shader_path, self._project_root())
print(f"Compiling shader: {relative_path}")
result = subprocess.run(
[glslc, shader_path, "-o", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode != 0:
raise RuntimeError(
f"failed to compile {shader_path}: {result.stderr.decode()}"
)
spirv = result.stdout
if len(spirv) % 4 != 0:
raise RuntimeError(
f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)"
)
word_count = len(spirv) // 4
words = struct.unpack(f"<{word_count}I", spirv)
filename = os.path.basename(shader_path)
variable_name = f"{filename.replace('.', '_')}_spv"
header_path = shader_path + ".spv.h"
body_lines = [
" " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + ","
for i in range(0, len(words), 4)
]
header = "\n".join([
"#pragma once",
"",
"#include <array>",
"#include <cstdint>",
"",
"namespace sophia::shaders {",
"",
f"inline constexpr std::array<uint32_t, {word_count}> {variable_name} = {{",
*body_lines,
"};",
"",
"} // namespace sophia::shaders",
"",
])
with open(header_path, "w") as f:
f.write(header)
print(f"Wrote {os.path.relpath(header_path, self._project_root())} "
f"({word_count * 4} bytes)")
def _wait_for_process_with_logging(self, command):
process = self._exec(command, capture=True)
for line in process.stdout:
@@ -433,4 +215,4 @@ class DevHelper:
if __name__ == "__main__":
DevHelper().call(*sys.argv[1:])
DevCommand().call(*sys.argv[1:])
+156
View File
@@ -0,0 +1,156 @@
import json
import re
import shlex
import shutil
import subprocess
import sys
# Mixed into DevCommand (bin/dev), which provides PROJECT_NAME, _project_root
# and _exec_in_root.
class GiteaCommand:
##
# Checks out a branch named after a Gitea issue URL.
# If the issue is from this project's repo, the branch name will be:
# issue-<issue_number>/<issue_title>
# If the issue is from another repo (presumably upstream), the branch name will be:
# <issue_owner>-issue-<issue_number>/<issue_title>
def branch_from(self, *args, **kwargs):
issue_url = args[0] if args else None
if issue_url is None:
raise ValueError("Must provide a Gitea issue URL.")
base_url, issue_repo, issue_number = self._parse_issue_url(issue_url)
issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number)
branch_name = self._format_branch_name(issue_repo, issue_number, issue_title)
exists = subprocess.run(
["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"],
stdout=subprocess.DEVNULL,
).returncode == 0
flag = "" if exists else "-b "
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)
##
# Creates an issue on this project's Gitea repo.
# dev issue <title> [-b|--body <text>] [-l|--label <name>]... [-a|--assignee <user>]... [-o|--open]
# The body is read from stdin if --body is not given and stdin is piped.
# Gitea's API cannot add issues to a project (only the web UI can), so the
# issue URL is printed and --open opens it in the browser to do so.
def issue(self, *args, **kwargs):
title, body, labels, assignees, open_in_browser = self._parse_issue_args(args)
repo = self._origin_repo()
if repo is None:
raise RuntimeError("could not determine the Gitea repo from the origin remote")
payload = {"title": title, "body": body}
if labels:
label_ids = self._fetch_label_ids(repo)
unknown = [label for label in labels if label not in label_ids]
if unknown:
raise ValueError(f"Unknown label(s): {', '.join(unknown)} (available: {', '.join(sorted(label_ids))})")
payload["labels"] = [label_ids[label] for label in labels]
if assignees:
payload["assignees"] = assignees
issue = self._tea_api(f"repos/{repo}/issues", payload)
print(f"Created issue #{issue['number']}: {issue['title']}")
print(issue["html_url"])
print(f"Add it to the project from the issue sidebar: Projects > {self.PROJECT_NAME}")
if open_in_browser:
opener = shutil.which("xdg-open") or shutil.which("open")
if opener is None:
raise RuntimeError("xdg-open not found on PATH")
subprocess.Popen([opener, issue["html_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _parse_issue_url(self, url):
match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
if match is None:
raise ValueError(f"Not a Gitea issue URL: {url}")
return match[1], f"{match[2]}/{match[3]}", match[4]
def _fetch_issue_title(self, base_url, repo, issue_number):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}"
print(f"Running: tea api {endpoint}")
result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True)
if result.returncode != 0:
raise RuntimeError(f"failed to fetch issue title from {endpoint}")
return json.loads(result.stdout)["title"]
def _parse_issue_args(self, args):
title = None
body = None
labels = []
assignees = []
open_in_browser = False
args = list(args)
while args:
arg = args.pop(0)
if arg in ("-b", "--body", "-l", "--label", "-a", "--assignee"):
if not args:
raise ValueError(f"{arg} requires a value")
value = args.pop(0)
if arg in ("-b", "--body"):
body = value
elif arg in ("-l", "--label"):
labels.append(value)
else:
assignees.append(value)
elif arg in ("-o", "--open"):
open_in_browser = True
elif arg.startswith("-"):
raise ValueError(f"Unknown option: {arg}")
elif title is None:
title = arg
else:
title = f"{title} {arg}"
if not title:
raise ValueError("Must provide an issue title. Usage: dev issue <title> [-b <body>] [-l <label>]... [-a <user>]... [-o]")
if body is None:
body = "" if sys.stdin.isatty() else sys.stdin.read()
return title, body, labels, assignees, open_in_browser
def _tea_api(self, endpoint, payload=None):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
command = ["tea", "api", endpoint]
if payload is not None:
command += ["-d", "@-"]
print(f"Running: {' '.join(command)}")
result = subprocess.run(
command,
input=json.dumps(payload) if payload is not None else None,
stdout=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"tea api request failed: {endpoint}")
return json.loads(result.stdout)
def _fetch_label_ids(self, repo):
labels = self._tea_api(f"repos/{repo}/labels?limit=100")
return {label["name"]: label["id"] for label in labels}
def _format_branch_name(self, issue_repo, issue_number, issue_title):
formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip())))
if issue_repo == self._origin_repo():
return f"issue-{issue_number}/{formatted_title}"
issue_owner = issue_repo.split("/")[0]
return f"{issue_owner}-issue-{issue_number}/{formatted_title}"
def _origin_repo(self):
result = subprocess.run(
["git", "-C", self._project_root(), "remote", "get-url", "origin"],
stdout=subprocess.PIPE,
text=True,
)
match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip())
return match[1] if match else None
+16
View File
@@ -0,0 +1,16 @@
import shutil
# Mixed into DevCommand (bin/dev), which provides _exec_in_root.
class RenderDocCommand:
def renderdoc(self, *args, **kwargs):
qrenderdoc = self._find_qrenderdoc()
self._exec_in_root(qrenderdoc, replace=True)
def _find_qrenderdoc(self):
qrenderdoc = shutil.which("qrenderdoc")
if qrenderdoc is None:
raise RuntimeError(
"qrenderdoc not found on PATH (install RenderDoc)"
)
return qrenderdoc
+92
View File
@@ -0,0 +1,92 @@
import os
import shutil
import struct
import subprocess
# Mixed into DevCommand (bin/dev), which provides _project_root.
class ShadersCommand:
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
def shaders(self, *args, **kwargs):
shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders")
if not os.path.isdir(shaders_dir):
raise ValueError(f"Shaders directory not found: {shaders_dir}")
glslc = self._find_glslc()
shader_paths = sorted(
os.path.join(root, filename)
for root, _dirs, files in os.walk(shaders_dir)
for filename in files
if filename.endswith(self.SHADER_EXTENSIONS)
)
if not shader_paths:
print(f"No shaders found in {shaders_dir}")
return
for shader_path in shader_paths:
self._generate_shader_header(glslc, shader_path)
def _find_glslc(self):
glslc = shutil.which("glslc")
if glslc is None:
raise RuntimeError(
"glslc not found on PATH (install the Vulkan SDK)"
)
return glslc
def _generate_shader_header(self, glslc, shader_path):
relative_path = os.path.relpath(shader_path, self._project_root())
print(f"Compiling shader: {relative_path}")
result = subprocess.run(
[glslc, shader_path, "-o", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode != 0:
raise RuntimeError(
f"failed to compile {shader_path}: {result.stderr.decode()}"
)
spirv = result.stdout
if len(spirv) % 4 != 0:
raise RuntimeError(
f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)"
)
word_count = len(spirv) // 4
words = struct.unpack(f"<{word_count}I", spirv)
filename = os.path.basename(shader_path)
variable_name = f"{filename.replace('.', '_')}_spv"
header_path = shader_path + ".spv.h"
body_lines = [
" " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + ","
for i in range(0, len(words), 4)
]
header = "\n".join([
"#pragma once",
"",
"#include <array>",
"#include <cstdint>",
"",
"namespace sophia::shaders {",
"",
f"inline constexpr std::array<uint32_t, {word_count}> {variable_name} = {{",
*body_lines,
"};",
"",
"} // namespace sophia::shaders",
"",
])
with open(header_path, "w") as f:
f.write(header)
print(f"Wrote {os.path.relpath(header_path, self._project_root())} "
f"({word_count * 4} bytes)")
+85
View File
@@ -0,0 +1,85 @@
import os
import re
import shlex
import time
# Mixed into DevCommand (bin/dev), which provides _project_root and
# _run_in_root.
class TracyCommand:
##
# Builds a Tracy tool from the third_party/tracy submodule, then runs it.
# Building from the submodule keeps the tool's version in sync with the
# client compiled into the engine.
# dev tracy [--fresh] [args...] Opens the profiler (viewer).
# dev tracy --save [name] [--fresh] [args...]
# Captures a trace headlessly to
# traces/<name>.tracy (default
# name: a timestamp) until the app
# exits or Ctrl+C.
# The profiler uses the X11 backend unless WAYLAND_DISPLAY is set. Tools
# are configured on first use (or --fresh, or when the backend changes),
# then built incrementally. Other arguments are passed through to the tool
# (e.g. a .tracy file to open, or `-s 10` to capture for 10 seconds).
def tracy(self, *args, **kwargs):
fresh, save, name, args = self._parse_tracy_args(args)
if save:
binary = self._build_tracy_tool("capture", "tracy-capture", {}, fresh)
traces_dir = os.path.join(self._project_root(), "traces")
os.makedirs(traces_dir, exist_ok=True)
name = name or time.strftime("%Y-%m-%d-%H%M%S")
if not name.endswith(".tracy"):
name += ".tracy"
args = ["-o", os.path.join(traces_dir, name), *args]
else:
legacy = "OFF" if os.environ.get("WAYLAND_DISPLAY") else "ON"
binary = self._build_tracy_tool("profiler", "tracy-profiler", {"LEGACY": legacy}, fresh)
print(f"Running: {shlex.join([binary, *args])}")
os.execv(binary, [binary, *args])
def _parse_tracy_args(self, args):
fresh = False
save = False
name = None
rest = []
args = list(args)
while args:
arg = args.pop(0)
if arg == "--fresh":
fresh = True
elif arg == "--save":
save = True
if args and not args[0].startswith("-"):
name = args.pop(0)
else:
rest.append(arg)
return fresh, save, name, rest
# Configures third_party/tracy/<tool> into <tool>/build when it has no
# build yet, --fresh is given, or a cached define differs from `defines`.
def _build_tracy_tool(self, tool, binary_name, defines, fresh):
source_dir = os.path.join("third_party", "tracy", tool)
build_dir = os.path.join(source_dir, "build")
cache_path = os.path.join(self._project_root(), build_dir, "CMakeCache.txt")
stale = not os.path.isfile(cache_path)
if not stale:
with open(cache_path) as f:
cache = f.read()
stale = any(
re.search(rf"^{key}:BOOL={value}$", cache, re.MULTILINE) is None
for key, value in defines.items()
)
if fresh or stale:
configure = ["cmake", "-B", build_dir, "-S", source_dir, "-DCMAKE_BUILD_TYPE=Release"]
configure += [f"-D{key}={value}" for key, value in defines.items()]
if fresh:
configure.append("--fresh")
self._run_in_root(configure)
self._run_in_root(["cmake", "--build", build_dir, "--parallel"])
return os.path.join(self._project_root(), build_dir, binary_name)
+2 -1
View File
@@ -7,9 +7,10 @@ add_library(${NAME}_deps INTERFACE)
target_link_libraries(${NAME}_deps INTERFACE
${ENGINE_NAME}
imguizmo
)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/build")
add_executable(editor ${SOURCES})
+99
View File
@@ -0,0 +1,99 @@
#include "editor_camera.hpp"
#include <algorithm>
#include <cmath>
namespace {
constexpr float ORBIT_RADIANS_PER_PIXEL = 0.005f;
const float MAX_PITCH = glm::radians(89.f);
// Each wheel step moves this fraction of the way towards the pivot.
constexpr float ZOOM_STEP = 0.1f;
constexpr float MIN_DISTANCE = 0.05f;
constexpr float MIN_ORTHO_HEIGHT = 0.01f;
// Fly speed per unit of distance from the pivot, so it suits any scale.
constexpr float FLY_SPEED = 1.5f;
} // namespace
void EditorCamera::orbit(glm::vec2 pixels) {
if (mode != Mode::Scene3D) {
return;
}
yaw -= pixels.x * ORBIT_RADIANS_PER_PIXEL;
pitch = std::clamp(pitch - pixels.y * ORBIT_RADIANS_PER_PIXEL, -MAX_PITCH,
MAX_PITCH);
}
void EditorCamera::pan(glm::vec2 pixels, float viewportHeight) {
if (viewportHeight <= 0.f) {
return;
}
// World units one pixel covers at the pivot's depth.
float unitsPerPixel =
mode == Mode::Scene3D
? 2.f * distance * std::tan(fov / 2.f) / viewportHeight
: orthoHeight / viewportHeight;
// Screen y runs down, world y up. Dragging moves the scene with the mouse,
// so the camera moves the other way.
glm::vec3 offset{-pixels.x * unitsPerPixel, pixels.y * unitsPerPixel, 0.f};
pivot += mode == Mode::Scene3D ? getRotation() * offset : offset;
}
void EditorCamera::zoom(float steps) {
float scale = std::pow(1.f - ZOOM_STEP, steps);
if (mode == Mode::Scene3D) {
distance = std::max(distance * scale, MIN_DISTANCE);
} else {
orthoHeight = std::max(orthoHeight * scale, MIN_ORTHO_HEIGHT);
}
}
void EditorCamera::fly(glm::vec3 direction, float deltaTime) {
if (mode != Mode::Scene3D || direction == glm::vec3{0.f}) {
return;
}
float speed = FLY_SPEED * std::max(distance, 1.f);
pivot += getRotation() * glm::normalize(direction) * speed * deltaTime;
}
void EditorCamera::frame(glm::vec3 target, float radius) {
pivot = target;
radius = std::max(radius, 0.1f);
// Far enough that the sphere fits in the vertical field of view.
distance = radius / std::sin(fov / 2.f);
orthoHeight = radius * 2.5f;
}
sophia::TransformComponent EditorCamera::getTransform() const {
sophia::TransformComponent transform;
if (mode == Mode::Scene3D) {
transform.rotation = getRotation();
transform.position =
pivot + transform.rotation * glm::vec3{0.f, 0.f, distance};
} else {
transform.position = pivot;
}
return transform;
}
sophia::CameraComponent EditorCamera::getCamera() const {
sophia::CameraComponent camera;
if (mode == Mode::Scene3D) {
camera.projection = sophia::CameraComponent::Projection::Perspective;
camera.perspective.fov = fov;
// Room to zoom far out without the scene clipping.
camera.perspective.farPlane = 1000.f;
} else {
camera.projection = sophia::CameraComponent::Projection::Orthographic;
camera.ortho.height = orthoHeight;
}
return camera;
}
glm::quat EditorCamera::getRotation() const {
return glm::angleAxis(yaw, glm::vec3{0.f, 1.f, 0.f}) *
glm::angleAxis(pitch, glm::vec3{1.f, 0.f, 0.f});
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <sophia/components.hpp>
#include <sophia/math.hpp>
/**
* The Scene panel's camera. Not part of any scene the editor edits, and never
* saved with one.
*
* In 3D it orbits a pivot point at some distance; in 2D it looks straight
* down -Z at the pivot through an orthographic projection, like a 2D game
* camera.
*/
class EditorCamera {
public:
enum class Mode { Scene3D, Scene2D };
Mode mode = Mode::Scene3D;
// Turns around the pivot by a mouse movement in pixels. 3D only.
void orbit(glm::vec2 pixels);
// Moves the pivot so the scene follows a mouse movement in pixels across a
// viewport `viewportHeight` pixels tall.
void pan(glm::vec2 pixels, float viewportHeight);
// Moves in (positive) or out by mouse wheel steps.
void zoom(float steps);
// Moves the pivot along the camera's axes: x right, y up, z backwards.
// `direction` needn't be normalized. 3D only.
void fly(glm::vec3 direction, float deltaTime);
// Centers on `target`, backing off enough to see a sphere of `radius`.
void frame(glm::vec3 target, float radius);
sophia::TransformComponent getTransform() const;
sophia::CameraComponent getCamera() const;
private:
glm::quat getRotation() const;
glm::vec3 pivot{0.f, 0.f, 0.f};
// 3D: looking at the pivot from in front, a little to the right and above.
float yaw = glm::radians(30.f);
float pitch = glm::radians(-25.f);
float distance = 8.f;
float fov = glm::radians(60.f);
// 2D: world units visible from the bottom of the view to the top.
float orthoHeight = 10.f;
};
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <sophia/entity.hpp>
#include <sophia/handles.hpp>
#include <sophia/scene.hpp>
#include <sophia/scene_registry.hpp>
#include <sophia/scene_view_registry.hpp>
#include <string>
#include <vector>
/**
* What the editor is working on, shared by its panels. Panels read it every
* frame instead of holding on to a scene, so switching scenes is one call.
*/
class EditorContext {
public:
// A scene the editor has open, and can switch to.
struct OpenScene {
sophia::SceneHandle handle;
std::string name;
};
explicit EditorContext(sophia::SceneRegistry& scenes) : scenes{scenes} {}
// Creates an open scene with a "Main Camera", without switching to it.
sophia::SceneHandle createScene(std::string name) {
sophia::SceneHandle handle = scenes.create();
scenes.get(handle).createCamera("Main Camera");
openScenes.push_back({handle, std::move(name)});
return handle;
}
const std::vector<OpenScene>& getOpenScenes() const { return openScenes; }
// Null if `handle` isn't an open scene.
const std::string* getSceneName(sophia::SceneHandle handle) const {
for (const OpenScene& open : openScenes) {
if (open.handle == handle) {
return &open.name;
}
}
return nullptr;
}
void renameScene(sophia::SceneHandle handle, std::string name) {
for (OpenScene& open : openScenes) {
if (open.handle == handle) {
open.name = std::move(name);
}
}
}
bool hasScene() const { return scenes.isAlive(activeScene); }
sophia::SceneHandle getSceneHandle() const { return activeScene; }
sophia::Scene& getScene() { return scenes.get(activeScene); }
// Also clears the selection, which belonged to the previous scene.
void setScene(sophia::SceneHandle handle) {
activeScene = handle;
selected = {};
}
// Null when nothing is selected. Always an entity of the active scene.
sophia::Entity selected;
private:
sophia::SceneRegistry& scenes;
sophia::SceneHandle activeScene = sophia::INVALID_HANDLE;
// Kept open even when not active: with no saving yet, closing one would
// lose it.
std::vector<OpenScene> openScenes;
};
// Points `view` at the context's active scene, and renders it only while
// there is one and its panel is `shown`. Call every frame from the panel that
// owns the view.
inline void syncSceneView(EditorContext& context,
sophia::SceneViewRegistry& sceneViews,
sophia::SceneViewHandle view, bool shown) {
bool enabled = shown && context.hasScene();
const sophia::SceneView& current = sceneViews.peek(view);
// get() re-sorts the render queue, so only use it when something changed.
if (current.enabled != enabled ||
(context.hasScene() && current.scene != context.getSceneHandle())) {
sophia::SceneView& updated = sceneViews.get(view);
updated.enabled = enabled;
updated.scene = context.getSceneHandle();
}
}
+127 -10
View File
@@ -1,36 +1,153 @@
#include <iostream>
#include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/primitives.hpp>
#include <vector>
#include "editor_context.hpp"
#include "panels/asset_browser_panel.hpp"
#include "panels/game_panel.hpp"
#include "panels/inspector_panel.hpp"
#include "panels/scene_hierarchy_panel.hpp"
#include "panels/viewport_panel.hpp"
#include "panels/scene_panel.hpp"
#include "utilities/logger.hpp"
class Editor : public sophia::Application {
public:
Editor() : Application{{.width = 750, .height = 1000, .name = "Editor"}} {}
void onCreate() override {
std::cout << "Editor created" << std::endl;
log("Editor created");
debugUI().show();
panels().emplace<SceneHierarchyPanel>();
panels().emplace<InspectorPanel>();
panels().emplace<AssetBrowserPanel>();
textures.emplace_back("Checker", createCheckerTexture());
cubeMesh = createShadedCube();
sophia::SceneHandle sceneHandle = context.createScene("Demo");
createDemoScene(scenes().get(sceneHandle));
context.setScene(sceneHandle);
// The editor's own scene, never edited or rendered. It holds the camera
// the Scene panel looks through, which a SceneView may take from any
// scene.
sophia::SceneHandle editorSceneHandle = scenes().create();
sophia::Entity editorCamera =
scenes().get(editorSceneHandle).createCamera("Editor Camera");
// Views start without a scene; their panels point them at the active
// scene each frame.
sophia::RenderTargetHandle sceneTarget =
resources().create<sophia::RenderTarget>();
views().create({.scene = scenes().create(), .target = sceneTarget});
panels().emplace<ViewportPanel>(ui(), sceneTarget);
sophia::SceneViewHandle sceneView = sceneViews().create(
{.target = sceneTarget, .camera = editorCamera, .enabled = false});
// No camera: the Game panel shows the scene through its active one.
sophia::RenderTargetHandle gameTarget =
resources().create<sophia::RenderTarget>();
sophia::SceneViewHandle gameView =
sceneViews().create({.target = gameTarget, .enabled = false});
panels().emplace<SceneHierarchyPanel>(context, cubeMesh);
panels().emplace<InspectorPanel>(context, textures);
panels().emplace<AssetBrowserPanel>();
viewPanels.push_back(
{panels().emplace<ScenePanel>(ui(), resources(), context, sceneViews(),
sceneView, sceneTarget, editorCamera),
sceneView});
viewPanels.push_back(
{panels().emplace<GamePanel>(ui(), context, sceneViews(), gameView,
gameTarget),
gameView});
panels().setDefaultLayout(
{.left = {"Scene Hierarchy"},
.right = {"Inspector", "Frame Stats", "PanelRegistry"},
.bottom = {"Asset Browser", "Console"},
.center = {"Viewport"}});
.center = {"Scene", "Game"}});
}
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
void onUpdate(const sophia::FrameInfo& frameInfo) override {
// A closed panel's onUpdate() doesn't run, so it can't stop its view
// rendering itself.
for (const ViewPanel& viewPanel : viewPanels) {
if (!panels().get(viewPanel.panel).isVisible() &&
sceneViews().peek(viewPanel.view).enabled) {
sceneViews().get(viewPanel.view).enabled = false;
}
}
}
private:
// Adds to a new scene, which already has a camera.
void createDemoScene(sophia::Scene& scene) {
sophia::Entity checker = scene.createEntity("Checker");
checker.getComponent<sophia::TransformComponent>().position = {-1.5f, 0.f,
0.f};
checker.addComponent<sophia::SpriteComponent>(sophia::SpriteComponent{
.texture = textures[0].second, .size = {2.f, 2.f}});
sophia::Entity red = scene.createEntity("Red");
red.getComponent<sophia::TransformComponent>().position = {1.f, -0.5f, 0.f};
red.addComponent<sophia::SpriteComponent>(sophia::SpriteComponent{
.color = {0.9f, 0.25f, 0.25f, 1.f}, .size = {2.f, 2.f}});
// Drawn over "Red" because of its higher sort order.
sophia::Entity blue = scene.createEntity("Blue (translucent)");
blue.getComponent<sophia::TransformComponent>().position = {1.75f, 0.25f,
0.f};
blue.addComponent<sophia::SpriteComponent>(
sophia::SpriteComponent{.color = {0.25f, 0.45f, 0.95f, 0.6f},
.size = {2.f, 2.f},
.sortOrder = 1});
sophia::Entity cube = scene.createEntity("Cube");
auto& cubeTransform = cube.getComponent<sophia::TransformComponent>();
cubeTransform.position = {0.f, 2.5f, 0.f};
cubeTransform.rotation =
glm::quat{glm::radians(glm::vec3{30.f, 45.f, 0.f})};
cube.addComponent<sophia::MeshComponent>(sophia::MeshComponent{
.handle = cubeMesh, .color = {0.4f, 0.8f, 0.5f, 1.f}});
}
// Meshes are unlit, so a plain cube is a flat silhouette. Bake a fixed
// light into its vertex colors so its faces read in 3D until there's real
// lighting; MeshComponent::color still tints it.
sophia::MeshHandle createShadedCube() {
const glm::vec3 lightDirection = glm::normalize(glm::vec3{0.4f, 1.f, 0.7f});
sophia::MeshData data = sophia::primitives::cube();
for (sophia::MeshVertex& vertex : data.vertices) {
float light = glm::dot(vertex.normal, lightDirection) * 0.5f + 0.5f;
vertex.color = glm::vec4{glm::vec3{0.35f + 0.65f * light}, 1.f};
}
return resources().create<sophia::Mesh>(data);
}
sophia::TextureHandle createCheckerTexture() {
constexpr sophia::u32 SIZE = 8;
std::vector<sophia::u8> pixels(SIZE * SIZE * 4);
for (sophia::u32 y = 0; y < SIZE; y++) {
for (sophia::u32 x = 0; x < SIZE; x++) {
sophia::u8 value = ((x + y) % 2 == 0) ? 230 : 60;
sophia::u8* pixel = &pixels[(y * SIZE + x) * 4];
pixel[0] = pixel[1] = pixel[2] = value;
pixel[3] = 255;
}
}
return resources().create<sophia::Texture>(SIZE, SIZE, std::move(pixels));
}
// A panel showing a scene view, which should only render while the panel
// is on screen.
struct ViewPanel {
sophia::PanelHandle panel;
sophia::SceneViewHandle view;
};
EditorContext context{scenes()};
std::vector<ViewPanel> viewPanels;
InspectorPanel::TextureList textures;
sophia::MeshHandle cubeMesh;
};
SOPHIA_MAIN(Editor)
+20
View File
@@ -0,0 +1,20 @@
#include "game_panel.hpp"
#include <imgui.h>
void GamePanel::onUpdate(const sophia::FrameInfo&) {
syncSceneView(context, sceneViews, view, isShown());
if (!context.hasScene()) {
ImGui::TextDisabled("No scene open");
return;
}
const ImVec2 origin = ImGui::GetCursorScreenPos();
ui.image(target);
if (!context.getScene().getActiveCamera()) {
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
ImGui::TextDisabled("The scene has no active camera");
}
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/scene_view_registry.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
#include "../editor_context.hpp"
/**
* Shows the active scene through its own active camera: what the game will
* see.
*/
class GamePanel : public sophia::Panel {
public:
// Renders into `target` through `view`, a SceneView with no camera of its
// own that this panel keeps pointed at the active scene.
GamePanel(sophia::UI& ui, EditorContext& context,
sophia::SceneViewRegistry& sceneViews, sophia::SceneViewHandle view,
sophia::RenderTargetHandle target)
: Panel{{.name = "Game",
.windowFlags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse}},
ui{ui},
context{context},
sceneViews{sceneViews},
view{view},
target{target} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
sophia::UI& ui;
EditorContext& context;
sophia::SceneViewRegistry& sceneViews;
sophia::SceneViewHandle view;
sophia::RenderTargetHandle target;
};
+147 -15
View File
@@ -3,30 +3,162 @@
#include <imgui.h>
#include <cstdio>
#include <sophia/components.hpp>
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
char nameBuf[128];
std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
name = nameBuf;
selected = context.selected;
if (!selected) {
ImGui::TextDisabled("No entity selected");
return;
}
ImGui::Text("Tag: %s", tag.c_str());
drawTag();
ImGui::Separator();
drawTransform();
drawCamera();
drawSprite();
drawMesh();
ImGui::Separator();
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::DragFloat3("Position", &position.x, 0.1f);
ImGui::DragFloat3("Rotation", &rotation.x, 0.1f);
ImGui::DragFloat3("Scale", &scale.x, 0.1f);
drawAddComponent();
}
if (ImGui::CollapsingHeader("Mesh Renderer",
void InspectorPanel::drawTag() {
auto& tag = selected.getComponent<sophia::TagComponent>();
char nameBuf[128];
std::snprintf(nameBuf, sizeof(nameBuf), "%s", tag.tag.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
tag.tag = nameBuf;
}
}
void InspectorPanel::drawTransform() {
if (!selected.hasComponent<sophia::TransformComponent>() ||
!ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
return;
}
auto& transform = selected.getComponent<sophia::TransformComponent>();
ImGui::DragFloat3("Position", &transform.position.x, 0.1f);
// Euler angles in degrees, recomputed from the quaternion every frame.
// Near 90 degrees of pitch the angles can jump between equivalent values
// while dragging.
// TODO: keep each entity's edited angles, or replace with gizmo rotation.
glm::vec3 degrees = glm::degrees(glm::eulerAngles(transform.rotation));
if (ImGui::DragFloat3("Rotation", &degrees.x, 1.0f)) {
transform.rotation = glm::quat{glm::radians(degrees)};
}
ImGui::DragFloat3("Scale", &transform.scale.x, 0.1f);
}
void InspectorPanel::drawCamera() {
if (!selected.hasComponent<sophia::CameraComponent>() ||
!ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
return;
}
auto& camera = selected.getComponent<sophia::CameraComponent>();
int projection = static_cast<int>(camera.projection);
if (ImGui::Combo("Projection", &projection, "Orthographic\0Perspective\0")) {
camera.projection =
static_cast<sophia::CameraComponent::Projection>(projection);
}
if (camera.projection == sophia::CameraComponent::Projection::Orthographic) {
ImGui::DragFloat("Height", &camera.ortho.height, 0.1f, 0.01f, 10000.f);
} else {
float fov = glm::degrees(camera.perspective.fov);
if (ImGui::DragFloat("FOV", &fov, 0.5f, 1.f, 179.f)) {
camera.perspective.fov = glm::radians(fov);
}
}
}
void InspectorPanel::drawSprite() {
if (!selected.hasComponent<sophia::SpriteComponent>()) {
return;
}
bool open = true;
if (!ImGui::CollapsingHeader("Sprite", &open,
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("Mesh: %s", meshName.c_str());
ImGui::Text("Material: %s", materialName.c_str());
if (!open) {
selected.removeComponent<sophia::SpriteComponent>();
}
return;
}
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("%s", scriptName.c_str());
auto& sprite = selected.getComponent<sophia::SpriteComponent>();
const char* current = "None";
for (const auto& [name, handle] : textures) {
if (handle == sprite.texture) {
current = name.c_str();
}
}
if (ImGui::BeginCombo("Texture", current)) {
if (ImGui::Selectable("None", !sprite.texture.isValid())) {
sprite.texture = sophia::INVALID_HANDLE;
}
for (const auto& [name, handle] : textures) {
if (ImGui::Selectable(name.c_str(), handle == sprite.texture)) {
sprite.texture = handle;
}
}
ImGui::EndCombo();
}
ImGui::ColorEdit4("Color", &sprite.color.x);
ImGui::DragFloat2("Size", &sprite.size.x, 0.05f);
ImGui::SliderFloat2("Pivot", &sprite.pivot.x, 0.f, 1.f);
ImGui::DragFloat4("UV Rect", &sprite.uvRect.x, 0.01f);
ImGui::DragInt("Sort Order", &sprite.sortOrder);
if (!open) {
selected.removeComponent<sophia::SpriteComponent>();
}
}
void InspectorPanel::drawMesh() {
if (!selected.hasComponent<sophia::MeshComponent>()) {
return;
}
bool open = true;
if (!ImGui::CollapsingHeader("Mesh", &open, ImGuiTreeNodeFlags_DefaultOpen)) {
if (!open) {
selected.removeComponent<sophia::MeshComponent>();
}
return;
}
auto& mesh = selected.getComponent<sophia::MeshComponent>();
// TODO: pick the mesh once assets have names.
ImGui::Text("Mesh: %u", static_cast<sophia::u32>(mesh.handle.index));
ImGui::ColorEdit3("Color", &mesh.color.x);
if (!open) {
selected.removeComponent<sophia::MeshComponent>();
}
}
void InspectorPanel::drawAddComponent() {
if (ImGui::Button("Add Component")) {
ImGui::OpenPopup("AddComponent");
}
if (ImGui::BeginPopup("AddComponent")) {
if (!selected.hasComponent<sophia::SpriteComponent>() &&
ImGui::MenuItem("Sprite")) {
selected.addComponent<sophia::SpriteComponent>();
}
if (!selected.hasComponent<sophia::CameraComponent>() &&
ImGui::MenuItem("Camera")) {
selected.addComponent<sophia::CameraComponent>();
}
ImGui::EndPopup();
}
}
+22 -11
View File
@@ -1,25 +1,36 @@
#pragma once
#include <glm/glm.hpp>
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <utility>
#include <vector>
#include "../editor_context.hpp"
class InspectorPanel : public sophia::Panel {
public:
InspectorPanel() : Panel{{.name = "Inspector"}} {}
// Stand-in for an asset browser: the textures a sprite can pick from.
using TextureList =
std::vector<std::pair<std::string, sophia::TextureHandle>>;
InspectorPanel(EditorContext& context, const TextureList& textures)
: Panel{{.name = "Inspector"}}, context{context}, textures{textures} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
std::string name = "Player";
std::string tag = "Player";
void drawTag();
void drawTransform();
void drawCamera();
void drawSprite();
void drawMesh();
void drawAddComponent();
glm::vec3 position{0.0f, 1.0f, 0.0f};
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
std::string meshName = "player_capsule.fbx";
std::string materialName = "player_mat";
std::string scriptName = "PlayerController.lua";
EditorContext& context;
// The selected entity, for the duration of onUpdate().
sophia::Entity selected;
const TextureList& textures;
};
+121 -19
View File
@@ -2,31 +2,133 @@
#include <imgui.h>
void SceneHierarchyPanel::drawNode(const Node& node) {
if (node.children.empty()) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen |
ImGuiTreeNodeFlags_SpanAvailWidth;
if (node.name == selected) {
flags |= ImGuiTreeNodeFlags_Selected;
}
ImGui::TreeNodeEx(node.name.c_str(), flags);
if (ImGui::IsItemClicked()) {
selected = node.name;
}
#include <cstdio>
#include <sophia/components.hpp>
#include <string>
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
drawSceneSelector();
ImGui::Separator();
if (!context.hasScene()) {
ImGui::TextDisabled("No scene open");
return;
}
if (ImGui::TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) {
for (const Node& child : node.children) {
drawNode(child);
sophia::Scene& scene = context.getScene();
sophia::Entity& selected = context.selected;
if (ImGui::Button("+ Sprite")) {
selected = scene.createEntity("Sprite");
selected.addComponent<sophia::SpriteComponent>();
}
ImGui::TreePop();
ImGui::SameLine();
if (ImGui::Button("+ Cube")) {
selected = scene.createEntity("Cube");
selected.addComponent<sophia::MeshComponent>(
sophia::MeshComponent{.handle = cubeMesh});
}
ImGui::Separator();
// Flat for now: there is no parent/child component yet.
scene.forEachEntity([&selected](sophia::Entity entity) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen |
ImGuiTreeNodeFlags_SpanAvailWidth;
if (entity == selected) {
flags |= ImGuiTreeNodeFlags_Selected;
}
const std::string& name = entity.getComponent<sophia::TagComponent>().tag;
ImGui::PushID(static_cast<int>(static_cast<sophia::u32>(entity)));
ImGui::TreeNodeEx(name.c_str(), flags);
if (ImGui::IsItemClicked()) {
selected = entity;
}
ImGui::PopID();
});
}
namespace {
// See SceneHierarchyPanel::renameFocusFrames.
constexpr int RENAME_FOCUS_FRAMES = 2;
} // namespace
void SceneHierarchyPanel::drawSceneSelector() {
// Leave room for the button: its label, its padding, and the gap to it.
constexpr const char* ADD_SCENE_LABEL = "Add Scene";
const ImGuiStyle& style = ImGui::GetStyle();
ImGui::SetNextItemWidth(-(ImGui::CalcTextSize(ADD_SCENE_LABEL).x +
style.FramePadding.x * 2.f + style.ItemSpacing.x));
const std::string* current = context.getSceneName(context.getSceneHandle());
bool startRename = false;
if (renamingScene && current) {
drawSceneRename();
} else {
renamingScene = false;
if (ImGui::BeginCombo("##scene", current ? current->c_str() : "No scene")) {
for (const EditorContext::OpenScene& open : context.getOpenScenes()) {
ImGui::PushID(static_cast<int>(open.handle.index));
bool isCurrent = open.handle == context.getSceneHandle();
if (ImGui::Selectable(open.name.c_str(), isCurrent) && !isCurrent) {
context.setScene(open.handle);
}
ImGui::PopID();
}
ImGui::EndCombo();
}
if (current && ImGui::BeginPopupContextItem("##sceneMenu")) {
startRename = ImGui::MenuItem("Rename", "F2");
ImGui::EndPopup();
}
startRename = startRename || (current && ImGui::IsWindowFocused() &&
!ImGui::GetIO().WantTextInput &&
ImGui::IsKeyPressed(ImGuiKey_F2));
}
ImGui::SameLine();
if (ImGui::Button(ADD_SCENE_LABEL)) {
std::string name = "Untitled " + std::to_string(++this->untitledSceneCount);
context.setScene(context.createScene(std::move(name)));
startRename = false;
}
if (startRename) {
renamingScene = true;
renameFocusFrames = RENAME_FOCUS_FRAMES;
std::snprintf(renameBuffer.data(), renameBuffer.size(), "%s",
current->c_str());
}
}
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
for (const Node& node : nodes) {
drawNode(node);
void SceneHierarchyPanel::drawSceneRename() {
if (renameFocusFrames == RENAME_FOCUS_FRAMES) {
ImGui::SetKeyboardFocusHere();
}
ImGui::InputText("##sceneName", renameBuffer.data(), renameBuffer.size(),
ImGuiInputTextFlags_AutoSelectAll);
bool active = ImGui::IsItemActive();
if (active) {
renameFocusFrames = 0;
} else if (renameFocusFrames > 0) {
renameFocusFrames--;
return;
}
// Escape reverts the text before deactivating, so committing whatever is
// left when the field closes covers Enter, clicking away and Escape alike.
if (!active) {
std::string name = renameBuffer.data();
// Keep the old name rather than leave the scene nameless.
if (name.find_first_not_of(' ') != std::string::npos) {
context.renameScene(context.getSceneHandle(), std::move(name));
}
renamingScene = false;
}
}
+24 -18
View File
@@ -1,32 +1,38 @@
#pragma once
#include <array>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <vector>
#include "../editor_context.hpp"
class SceneHierarchyPanel : public sophia::Panel {
public:
SceneHierarchyPanel() : Panel{{.name = "Scene Hierarchy", .locked = true}} {}
// `cubeMesh` is what "+ Cube" gives new entities.
SceneHierarchyPanel(EditorContext& context, sophia::MeshHandle cubeMesh)
: Panel{{.name = "Scene Hierarchy", .locked = true}},
context{context},
cubeMesh{cubeMesh} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
struct Node {
std::string name;
std::vector<Node> children;
};
// Switches between open scenes, renames the active one, and creates new
// ones.
void drawSceneSelector();
// Replaces the selector with a text field editing the active scene's name.
void drawSceneRename();
void drawNode(const Node& node);
EditorContext& context;
sophia::MeshHandle cubeMesh;
// For naming new scenes "Untitled 1", "Untitled 2", ...
int untitledSceneCount = 0;
std::vector<Node> nodes = {
{"Main Camera", {}},
{"Directional Light", {}},
{"Ground", {}},
{"Player", {}},
{"Enemies", {{"Enemy_01", {}}, {"Enemy_02", {}}, {"Enemy_03", {}}}},
{"Props", {{"Crate_01", {}}, {"Crate_02", {}}}},
};
std::string selected;
// Set while the scene selector is a text field for renaming.
bool renamingScene = false;
// Frames the text field has left to become active after renaming starts;
// focus can take a frame to land. If it never does, renaming ends.
int renameFocusFrames = 0;
std::array<char, 128> renameBuffer{};
};
+603
View File
@@ -0,0 +1,603 @@
#include "scene_panel.hpp"
#include <imgui.h>
// After imgui.h, which it relies on.
#include <ImGuizmo.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <glm/gtc/type_ptr.hpp>
#include <limits>
#include <optional>
#include <sophia/aabb.hpp>
#include <sophia/components.hpp>
#include <tuple>
#include <utility>
#include <vector>
namespace {
// Corners of the unit quad a SpriteComponent's model matrix maps to world.
constexpr std::array<glm::vec2, 4> UNIT_QUAD{
glm::vec2{0.f, 0.f}, glm::vec2{1.f, 0.f}, glm::vec2{1.f, 1.f},
glm::vec2{0.f, 1.f}};
const ImU32 SELECTION_COLOR = IM_COL32(255, 170, 40, 255);
// Holding Ctrl snaps the gizmo to these steps.
constexpr float TRANSLATE_SNAP = 0.5f;
constexpr float ROTATE_SNAP_DEGREES = 15.f;
constexpr float SCALE_SNAP = 0.1f;
// Writes a matrix built from a translation, rotation and scale (no shear)
// back into `transform`. A mirroring matrix comes back with a negative x
// scale.
void decompose(const glm::mat4& matrix, sophia::TransformComponent& transform) {
transform.position = glm::vec3{matrix[3]};
glm::vec3 scale{glm::length(glm::vec3{matrix[0]}),
glm::length(glm::vec3{matrix[1]}),
glm::length(glm::vec3{matrix[2]})};
if (glm::determinant(glm::mat3{matrix}) < 0.f) {
scale.x = -scale.x;
}
transform.scale = scale;
// A zero scale loses the axis, and with it the rotation; keep the old one.
if (scale.x == 0.f || scale.y == 0.f || scale.z == 0.f) {
return;
}
glm::mat3 rotation{glm::vec3{matrix[0]} / scale.x,
glm::vec3{matrix[1]} / scale.y,
glm::vec3{matrix[2]} / scale.z};
transform.rotation = glm::normalize(glm::quat_cast(rotation));
}
// How far along a ray (from `origin`, along the normalized `direction`) it
// enters `bounds` placed by `model`, or null if it misses. A ray starting
// inside hits at 0.
std::optional<float> intersectBox(const glm::vec3& origin,
const glm::vec3& direction,
const glm::mat4& model,
const sophia::AABB& bounds) {
if (glm::determinant(model) == 0.f) {
return std::nullopt;
}
// Test in the box's space, where it's axis-aligned. The direction isn't
// renormalized, so distances along it are still world distances.
glm::mat4 inverse = glm::inverse(model);
glm::vec3 localOrigin = glm::vec3{inverse * glm::vec4{origin, 1.f}};
glm::vec3 localDirection = glm::vec3{inverse * glm::vec4{direction, 0.f}};
float enter = 0.f;
float exit = std::numeric_limits<float>::infinity();
for (int axis = 0; axis < 3; axis++) {
if (std::abs(localDirection[axis]) < 1e-8f) {
// Parallel to this pair of faces: hits only if already between them.
if (localOrigin[axis] < bounds.min[axis] ||
localOrigin[axis] > bounds.max[axis]) {
return std::nullopt;
}
continue;
}
float toMin = (bounds.min[axis] - localOrigin[axis]) / localDirection[axis];
float toMax = (bounds.max[axis] - localOrigin[axis]) / localDirection[axis];
if (toMin > toMax) {
std::swap(toMin, toMax);
}
enter = std::max(enter, toMin);
exit = std::min(exit, toMax);
if (enter > exit) {
return std::nullopt;
}
}
return enter;
}
// How far along a ray it hits the unit quad (0, 0)-(1, 1) in the XY plane,
// placed by `model`, or null if it misses.
std::optional<float> intersectQuad(const glm::vec3& origin,
const glm::vec3& direction,
const glm::mat4& model) {
if (glm::determinant(model) == 0.f) {
return std::nullopt;
}
glm::mat4 inverse = glm::inverse(model);
glm::vec3 localOrigin = glm::vec3{inverse * glm::vec4{origin, 1.f}};
glm::vec3 localDirection = glm::vec3{inverse * glm::vec4{direction, 0.f}};
// Edge on to the quad.
if (std::abs(localDirection.z) < 1e-8f) {
return std::nullopt;
}
float distance = -localOrigin.z / localDirection.z;
if (distance < 0.f) {
return std::nullopt;
}
glm::vec3 hit = localOrigin + distance * localDirection;
if (hit.x < 0.f || hit.x > 1.f || hit.y < 0.f || hit.y > 1.f) {
return std::nullopt;
}
return distance;
}
} // namespace
ImVec2 ScenePanel::ViewportTransform::clipToScreen(
const glm::vec4& clip) const {
glm::vec2 ndc = glm::vec2{clip} / clip.w;
// Vulkan NDC has +y pointing down, same as the screen.
return {origin.x + (ndc.x * 0.5f + 0.5f) * size.x,
origin.y + (ndc.y * 0.5f + 0.5f) * size.y};
}
std::optional<ImVec2> ScenePanel::ViewportTransform::worldToScreen(
const glm::vec3& world) const {
glm::vec4 clip = viewProjection * glm::vec4{world, 1.f};
if (clip.w <= 0.f) {
return std::nullopt;
}
return clipToScreen(clip);
}
glm::vec2 ScenePanel::ViewportTransform::screenToWorld(
const ImVec2& screen) const {
glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f,
(screen.y - origin.y) / size.y * 2.f - 1.f};
glm::vec4 world = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f};
return glm::vec2{world} / world.w;
}
ScenePanel::Ray ScenePanel::ViewportTransform::screenToRay(
const ImVec2& screen) const {
glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f,
(screen.y - origin.y) / size.y * 2.f - 1.f};
// Depth runs 0 at the near plane to 1 at the far one.
glm::vec4 nearPoint = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f};
glm::vec4 farPoint = inverseViewProjection * glm::vec4{ndc, 1.f, 1.f};
glm::vec3 from = glm::vec3{nearPoint} / nearPoint.w;
glm::vec3 to = glm::vec3{farPoint} / farPoint.w;
return {from, glm::normalize(to - from)};
}
void ScenePanel::ViewportTransform::drawLine(ImDrawList* drawList,
const glm::vec3& a,
const glm::vec3& b, ImU32 color,
float thickness) const {
// Just in front of the camera. Points behind it project mirrored, so the
// part of the line there is cut off rather than drawn.
constexpr float MIN_W = 1e-4f;
glm::vec4 clipA = viewProjection * glm::vec4{a, 1.f};
glm::vec4 clipB = viewProjection * glm::vec4{b, 1.f};
if (clipA.w < MIN_W && clipB.w < MIN_W) {
return;
}
if (clipA.w < MIN_W) {
clipA = glm::mix(clipA, clipB, (MIN_W - clipA.w) / (clipB.w - clipA.w));
} else if (clipB.w < MIN_W) {
clipB = glm::mix(clipB, clipA, (MIN_W - clipB.w) / (clipA.w - clipB.w));
}
drawList->AddLine(clipToScreen(clipA), clipToScreen(clipB), color, thickness);
}
void ScenePanel::onUpdate(const sophia::FrameInfo& frameInfo) {
syncSceneView(context, sceneViews, view, isShown());
drawToolbar();
ImGui::Separator();
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 size = ImGui::GetContentRegionAvail();
if (size.x < 1.0f || size.y < 1.0f) {
return;
}
if (!context.hasScene()) {
ImGui::TextDisabled("No scene open");
return;
}
ui.image(target);
// Rolls over ImGuizmo's "mouse is over a handle" state, so IsOver() below
// reports last frame's.
ImGuizmo::BeginFrame();
// A handle can't be grabbed while any ImGui item is hovered, so while the
// mouse is over one, leave out the viewport button, and keep the window
// still so dragging the handle doesn't drag it. Unless a camera drag is
// already under way: dropping the button would end it.
bool overGizmo =
(ImGuizmo::IsOver() || ImGuizmo::IsUsing()) && !viewportActive;
getConfig().windowFlags =
overGizmo ? WINDOW_FLAGS | ImGuiWindowFlags_NoMove : WINDOW_FLAGS;
// The image doesn't take mouse input, so without this, dragging in it
// would drag the window instead.
ImGui::SetCursorScreenPos(origin);
if (overGizmo) {
ImGui::Dummy(size);
} else {
ImGui::InvisibleButton("##viewport", size,
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonRight |
ImGuiButtonFlags_MouseButtonMiddle);
}
viewportActive = !overGizmo && ImGui::IsItemActive();
handleCameraInput(frameInfo, size);
handleGizmoHotkeys();
// Drive the view's camera from the editor camera. The scene view renders
// after the panels update, so this frame already uses it.
camera.getComponent<sophia::TransformComponent>() =
editorCamera.getTransform();
camera.getComponent<sophia::CameraComponent>() = editorCamera.getCamera();
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y),
true);
// Screen-space, so it only lines up with anything in 2D.
// TODO: a world-space grid on the XZ plane for 3D, once there's debug
// drawing in scene views.
if (showGrid && editorCamera.mode == EditorCamera::Mode::Scene2D) {
drawGrid(origin, size);
}
// Use the panel's size rather than the target's: the target is resized a
// frame late, and the mouse is over the panel.
float aspect = size.x / size.y;
glm::mat4 view = sophia::CameraComponent::getView(
camera.getComponent<sophia::TransformComponent>());
glm::mat4 viewProjection =
camera.getComponent<sophia::CameraComponent>().getProjection(aspect) *
view;
ViewportTransform viewport{origin, size, viewProjection,
glm::inverse(viewProjection)};
handleSelectionInput(viewport);
drawSelection(viewport);
ImGuizmo::SetDrawlist();
ImGuizmo::SetRect(origin.x, origin.y, size.x, size.y);
drawGizmo(view, aspect);
drawList->PopClipRect();
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
}
void ScenePanel::drawToolbar() {
bool is2D = editorCamera.mode == EditorCamera::Mode::Scene2D;
if (ImGui::Checkbox("2D", &is2D)) {
editorCamera.mode =
is2D ? EditorCamera::Mode::Scene2D : EditorCamera::Mode::Scene3D;
dragging = false;
}
ImGui::SameLine();
ImGui::BeginDisabled(!is2D);
ImGui::Checkbox("Grid", &showGrid);
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
const struct {
const char* label;
GizmoOperation operation;
} operations[] = {{"Move (W)", GizmoOperation::Translate},
{"Rotate (E)", GizmoOperation::Rotate},
{"Scale (R)", GizmoOperation::Scale}};
for (const auto& entry : operations) {
if (ImGui::RadioButton(entry.label, gizmoOperation == entry.operation)) {
gizmoOperation = entry.operation;
}
ImGui::SameLine();
}
if (ImGui::Button(gizmoLocal ? "Local" : "World")) {
gizmoLocal = !gizmoLocal;
}
}
void ScenePanel::handleGizmoHotkeys() {
const ImGuiIO& io = ImGui::GetIO();
// W/E/Q fly the camera while the right button is held.
if (!ImGui::IsWindowFocused() || io.WantTextInput ||
ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
gizmoOperation = GizmoOperation::Translate;
} else if (ImGui::IsKeyPressed(ImGuiKey_E, false)) {
gizmoOperation = GizmoOperation::Rotate;
} else if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
gizmoOperation = GizmoOperation::Scale;
}
}
void ScenePanel::drawGizmo(const glm::mat4& view, float aspect) {
sophia::Entity selected = context.selected;
if (!selected) {
return;
}
bool is2D = editorCamera.mode == EditorCamera::Mode::Scene2D;
// In 2D, only the axes that keep things in the XY plane.
ImGuizmo::OPERATION operation{};
float snapStep = 0.f;
switch (gizmoOperation) {
case GizmoOperation::Translate:
operation = is2D ? ImGuizmo::TRANSLATE_X | ImGuizmo::TRANSLATE_Y
: ImGuizmo::TRANSLATE;
snapStep = TRANSLATE_SNAP;
break;
case GizmoOperation::Rotate:
operation = is2D ? ImGuizmo::ROTATE_Z : ImGuizmo::ROTATE;
snapStep = ROTATE_SNAP_DEGREES;
break;
case GizmoOperation::Scale:
operation =
is2D ? ImGuizmo::SCALE_X | ImGuizmo::SCALE_Y : ImGuizmo::SCALE;
snapStep = SCALE_SNAP;
break;
}
std::array<float, 3> snap{snapStep, snapStep, snapStep};
bool snapping = ImGui::GetIO().KeyCtrl;
// ImGuizmo maps clip space to the screen the OpenGL way, with +y up, so it
// needs the projection without the Y flip CameraComponent adds for Vulkan.
glm::mat4 projection =
camera.getComponent<sophia::CameraComponent>().getProjection(aspect);
projection[1][1] *= -1.f;
auto& transform = selected.getComponent<sophia::TransformComponent>();
glm::mat4 model = transform.mat4();
ImGuizmo::SetOrthographic(is2D);
// Per entity, so a drag in progress doesn't carry over to a new selection.
ImGuizmo::PushID(static_cast<int>(static_cast<sophia::u32>(selected)));
bool changed = ImGuizmo::Manipulate(
glm::value_ptr(view), glm::value_ptr(projection), operation,
gizmoLocal ? ImGuizmo::LOCAL : ImGuizmo::WORLD, glm::value_ptr(model),
nullptr, snapping ? snap.data() : nullptr);
ImGuizmo::PopID();
if (changed) {
decompose(model, transform);
}
}
void ScenePanel::drawGrid(ImVec2 origin, ImVec2 size) {
constexpr float GRID_STEP = 40.0f;
const ImU32 gridColor = IM_COL32(255, 255, 255, 18);
const ImVec2 end(origin.x + size.x, origin.y + size.y);
ImDrawList* drawList = ImGui::GetWindowDrawList();
for (float x = GRID_STEP; x < size.x; x += GRID_STEP) {
drawList->AddLine(ImVec2(origin.x + x, origin.y),
ImVec2(origin.x + x, end.y), gridColor);
}
for (float y = GRID_STEP; y < size.y; y += GRID_STEP) {
drawList->AddLine(ImVec2(origin.x, origin.y + y),
ImVec2(end.x, origin.y + y), gridColor);
}
}
void ScenePanel::handleCameraInput(const sophia::FrameInfo& frameInfo,
ImVec2 size) {
const ImGuiIO& io = ImGui::GetIO();
bool is3D = editorCamera.mode == EditorCamera::Mode::Scene3D;
glm::vec2 mouseDelta{io.MouseDelta.x, io.MouseDelta.y};
// The button stays active while any of its mouse buttons is held, even
// once the mouse leaves the panel.
if (ImGui::IsItemActive()) {
bool rightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
bool middleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
if (is3D && rightDown) {
editorCamera.orbit(mouseDelta);
glm::vec3 direction{0.f};
if (ImGui::IsKeyDown(ImGuiKey_D)) direction.x += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_A)) direction.x -= 1.f;
if (ImGui::IsKeyDown(ImGuiKey_E)) direction.y += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_Q)) direction.y -= 1.f;
if (ImGui::IsKeyDown(ImGuiKey_S)) direction.z += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_W)) direction.z -= 1.f;
editorCamera.fly(direction, static_cast<float>(frameInfo.deltaTime));
} else if (middleDown || (!is3D && rightDown)) {
editorCamera.pan(mouseDelta, size.y);
}
}
if (ImGui::IsItemHovered() && io.MouseWheel != 0.f) {
editorCamera.zoom(io.MouseWheel);
}
if (ImGui::IsWindowFocused() && !io.WantTextInput &&
ImGui::IsKeyPressed(ImGuiKey_F, false)) {
frameSelection();
}
}
void ScenePanel::handleSelectionInput(const ViewportTransform& viewport) {
const ImGuiIO& io = ImGui::GetIO();
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
context.selected = pick(viewport.screenToRay(io.MousePos));
// Dragging the selection around is 2D only; in 3D, use the gizmo.
dragging =
context.selected && editorCamera.mode == EditorCamera::Mode::Scene2D;
}
if (!ImGui::IsItemActive() || !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
dragging = false;
return;
}
if (dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) {
ImVec2 previous(io.MousePos.x - io.MouseDelta.x,
io.MousePos.y - io.MouseDelta.y);
glm::vec2 delta =
viewport.screenToWorld(io.MousePos) - viewport.screenToWorld(previous);
auto& transform =
context.selected.getComponent<sophia::TransformComponent>();
transform.position.x += delta.x;
transform.position.y += delta.y;
}
}
void ScenePanel::drawSelection(const ViewportTransform& viewport) {
sophia::Entity selected = context.selected;
if (!selected) {
return;
}
ImDrawList* drawList = ImGui::GetWindowDrawList();
const auto& transform = selected.getComponent<sophia::TransformComponent>();
if (selected.hasComponent<sophia::SpriteComponent>()) {
glm::mat4 model =
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
for (size_t i = 0; i < UNIT_QUAD.size(); i++) {
const glm::vec2& from = UNIT_QUAD[i];
const glm::vec2& to = UNIT_QUAD[(i + 1) % UNIT_QUAD.size()];
viewport.drawLine(drawList, glm::vec3{model * glm::vec4{from, 0.f, 1.f}},
glm::vec3{model * glm::vec4{to, 0.f, 1.f}},
SELECTION_COLOR, 2.0f);
}
}
if (std::optional<sophia::AABB> bounds = getMeshBounds(selected)) {
glm::mat4 model = transform.mat4();
std::array<glm::vec3, 8> corners = bounds->getCorners();
for (glm::vec3& corner : corners) {
corner = glm::vec3{model * glm::vec4{corner, 1.f}};
}
// Corners whose indices differ in one bit share an edge.
for (int i = 0; i < 8; i++) {
for (int bit : {1, 2, 4}) {
if ((i & bit) == 0) {
viewport.drawLine(drawList, corners[i], corners[i | bit],
SELECTION_COLOR, 2.0f);
}
}
}
}
// Mark the transform's position (the sprite's pivot), which is also all
// there is to show for entities without a sprite or mesh, such as the
// camera.
if (std::optional<ImVec2> center =
viewport.worldToScreen(transform.position)) {
drawList->AddCircle(*center, 4.0f, SELECTION_COLOR, 0, 2.0f);
}
}
void ScenePanel::frameSelection() {
sophia::Entity selected = context.selected;
if (!selected) {
return;
}
const auto& transform = selected.getComponent<sophia::TransformComponent>();
// The selection's corners in world space, from its mesh or sprite.
std::vector<glm::vec3> points;
if (std::optional<sophia::AABB> bounds = getMeshBounds(selected)) {
glm::mat4 model = transform.mat4();
for (const glm::vec3& corner : bounds->getCorners()) {
points.push_back(glm::vec3{model * glm::vec4{corner, 1.f}});
}
}
if (selected.hasComponent<sophia::SpriteComponent>()) {
glm::mat4 model =
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
for (const glm::vec2& corner : UNIT_QUAD) {
points.push_back(glm::vec3{model * glm::vec4{corner, 0.f, 1.f}});
}
}
if (points.empty()) {
// Nothing with a size: frame a unit around its position.
editorCamera.frame(transform.position, 0.5f);
return;
}
glm::vec3 min = points.front();
glm::vec3 max = points.front();
for (const glm::vec3& point : points) {
min = glm::min(min, point);
max = glm::max(max, point);
}
editorCamera.frame((min + max) * 0.5f, glm::length(max - min) * 0.5f);
}
std::optional<sophia::AABB> ScenePanel::getMeshBounds(sophia::Entity entity) {
if (!entity.hasComponent<sophia::MeshComponent>()) {
return std::nullopt;
}
sophia::MeshHandle mesh = entity.getComponent<sophia::MeshComponent>().handle;
if (!resources.isAlive(mesh)) {
return std::nullopt;
}
return resources.getBounds(mesh);
}
sophia::Entity ScenePanel::pick(const Ray& ray) {
sophia::Scene& scene = context.getScene();
// Meshes are opaque and depth tested, so the nearest one hides anything
// behind it.
sophia::Entity nearestMesh;
float meshDistance = std::numeric_limits<float>::infinity();
scene.forEachEntity([&](sophia::Entity entity) {
std::optional<sophia::AABB> bounds = getMeshBounds(entity);
if (!bounds) {
return;
}
glm::mat4 model = entity.getComponent<sophia::TransformComponent>().mat4();
std::optional<float> distance =
intersectBox(ray.origin, ray.direction, model, *bounds);
if (distance && *distance < meshDistance) {
nearestMesh = entity;
meshDistance = *distance;
}
});
// Sprites in front of that mesh draw over it, in the renderer's order:
// higher sortOrder on top, then nearer.
sophia::Entity topSprite;
std::optional<std::tuple<sophia::s32, float>> topOrder;
scene.forEachEntity([&](sophia::Entity entity) {
if (!entity.hasComponent<sophia::SpriteComponent>()) {
return;
}
const auto& sprite = entity.getComponent<sophia::SpriteComponent>();
glm::mat4 model =
sprite.getModel(entity.getComponent<sophia::TransformComponent>());
std::optional<float> distance =
intersectQuad(ray.origin, ray.direction, model);
if (!distance || *distance > meshDistance) {
return;
}
// Negated distance, so nearer compares greater.
std::tuple order{sprite.sortOrder, -*distance};
if (!topOrder || order > *topOrder) {
topSprite = entity;
topOrder = order;
}
});
return topSprite ? topSprite : nearestMesh;
}
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <optional>
#include <sophia/aabb.hpp>
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/resource_registry_ref.hpp>
#include <sophia/scene_view_registry.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
#include "../editor_camera.hpp"
#include "../editor_context.hpp"
/**
* Shows the active scene through the editor's own camera.
*
* 3D: right-drag orbits, middle-drag pans, the wheel zooms, right-drag with
* WASD/QE flies. 2D: right- or middle-drag pans, the wheel zooms. F frames
* the selection in both.
*
* The selection gets a transform gizmo: W moves, E rotates, R scales, and
* holding Ctrl snaps. Clicking selects the mesh or sprite under the mouse;
* in 2D, dragging it also moves it.
*/
class ScenePanel : public sophia::Panel {
public:
// Renders into `target` through `view`, a SceneView this panel keeps
// pointed at the active scene. `camera` is the entity the view looks
// through; it belongs to a scene of the editor's own, and this panel
// drives it from its EditorCamera.
ScenePanel(sophia::UI& ui, sophia::ResourceRegistryRef resources,
EditorContext& context, sophia::SceneViewRegistry& sceneViews,
sophia::SceneViewHandle view, sophia::RenderTargetHandle target,
sophia::Entity camera)
: Panel{{.name = "Scene", .windowFlags = WINDOW_FLAGS}},
ui{ui},
resources{resources},
context{context},
sceneViews{sceneViews},
view{view},
target{target},
camera{camera} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
static constexpr ImGuiWindowFlags WINDOW_FLAGS =
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
enum class GizmoOperation { Translate, Rotate, Scale };
// A half-line in world space. `direction` is normalized, so distances
// along it are in world units.
struct Ray {
glm::vec3 origin;
glm::vec3 direction;
};
// Where the scene image is on screen this frame, and the camera looking
// through it.
struct ViewportTransform {
ImVec2 origin;
ImVec2 size;
glm::mat4 viewProjection;
glm::mat4 inverseViewProjection;
// Null for points behind the camera.
std::optional<ImVec2> worldToScreen(const glm::vec3& world) const;
// Where `screen` is on the camera's near plane. Only the x and y, which
// is all that's needed in 2D.
glm::vec2 screenToWorld(const ImVec2& screen) const;
// From the near plane through `screen`, into the scene.
Ray screenToRay(const ImVec2& screen) const;
// Draws a world-space line, cut off where it passes behind the camera.
void drawLine(ImDrawList* drawList, const glm::vec3& a, const glm::vec3& b,
ImU32 color, float thickness) const;
private:
ImVec2 clipToScreen(const glm::vec4& clip) const;
};
void drawToolbar();
void drawGrid(ImVec2 origin, ImVec2 size);
void handleCameraInput(const sophia::FrameInfo& frameInfo, ImVec2 size);
void handleSelectionInput(const ViewportTransform& viewport);
void handleGizmoHotkeys();
// Draws the transform gizmo on the selection, and applies any change made
// with it. `view` is the editor camera's, and `aspect` the viewport's
// width over its height.
void drawGizmo(const glm::mat4& view, float aspect);
void drawSelection(const ViewportTransform& viewport);
void frameSelection();
// The bounds of the entity's mesh in the mesh's own space, if it has one.
std::optional<sophia::AABB> getMeshBounds(sophia::Entity entity);
// The entity drawn in front along `ray`, or a null Entity. Only meshes and
// sprites can be hit.
sophia::Entity pick(const Ray& ray);
sophia::UI& ui;
sophia::ResourceRegistryRef resources;
EditorContext& context;
sophia::SceneViewRegistry& sceneViews;
sophia::SceneViewHandle view;
sophia::RenderTargetHandle target;
sophia::Entity camera;
EditorCamera editorCamera;
GizmoOperation gizmoOperation = GizmoOperation::Translate;
// Whether the gizmo works along the entity's own axes or the world's.
bool gizmoLocal = true;
// Whether the viewport button was active last frame, i.e. a camera drag or
// click is under way, which the gizmo mustn't interrupt.
bool viewportActive = false;
bool showGrid = true;
// Set while a drag that started on the selected entity is moving it.
bool dragging = false;
};
-46
View File
@@ -1,46 +0,0 @@
#include "viewport_panel.hpp"
#include <imgui.h>
void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
const struct {
const char* label;
GizmoMode mode;
} modes[] = {{"Move", GizmoMode::Move},
{"Rotate", GizmoMode::Rotate},
{"Scale", GizmoMode::Scale}};
for (const auto& entry : modes) {
if (ImGui::RadioButton(entry.label, gizmoMode == entry.mode)) {
gizmoMode = entry.mode;
}
ImGui::SameLine();
}
ImGui::Checkbox("Grid", &showGrid);
ImGui::Separator();
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 size = ImGui::GetContentRegionAvail();
const ImVec2 end(origin.x + size.x, origin.y + size.y);
ui.image(target);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (showGrid) {
constexpr float GRID_STEP = 40.0f;
const ImU32 gridColor = IM_COL32(255, 255, 255, 18);
for (float x = GRID_STEP; x < size.x; x += GRID_STEP) {
drawList->AddLine(ImVec2(origin.x + x, origin.y),
ImVec2(origin.x + x, end.y), gridColor);
}
for (float y = GRID_STEP; y < size.y; y += GRID_STEP) {
drawList->AddLine(ImVec2(origin.x, origin.y + y),
ImVec2(end.x, origin.y + y), gridColor);
}
}
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
}
-27
View File
@@ -1,27 +0,0 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
class ViewportPanel : public sophia::Panel {
public:
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
: Panel{{.name = "Viewport",
.windowFlags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse}},
ui{ui},
target{target} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
enum class GizmoMode { Move, Rotate, Scale };
GizmoMode gizmoMode = GizmoMode::Move;
bool showGrid = true;
};
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <format>
#include <sophia/util/logger.hpp>
#include <utility>
template <typename... Args>
void log(std::format_string<Args...> fmt, Args&&... args) {
if constexpr (sophia::log::enabled) {
sophia::log::print("\033[1;38;2;170;90;255m[editor]\033[0m {}",
std::format(fmt, std::forward<Args>(args)...));
}
}
+2
View File
@@ -24,11 +24,13 @@ target_compile_definitions(${ENGINE_NAME} PUBLIC
target_link_libraries(${ENGINE_NAME} PUBLIC
vulkan
vma
shaderc
glfw
glm
imgui
entt
Tracy::TracyClient
)
target_link_libraries(${ENGINE_NAME} PRIVATE
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <array>
#include <sophia/math.hpp>
namespace sophia {
/** @brief Axis-Aligned Bounding Box
*
* An axis-aligned box, from its smallest corner to its largest.
*/
struct AABB {
vec3 min{0.f, 0.f, 0.f};
vec3 max{0.f, 0.f, 0.f};
vec3 getCenter() const { return (min + max) * 0.5f; }
vec3 getSize() const { return max - min; }
// The 8 corners. Bit 0 of the index picks x from max, bit 1 y, bit 2 z.
std::array<vec3, 8> getCorners() const {
std::array<vec3, 8> corners;
for (int i = 0; i < 8; i++) {
corners[i] = {(i & 1) ? max.x : min.x, (i & 2) ? max.y : min.y,
(i & 4) ? max.z : min.z};
}
return corners;
}
};
} // namespace sophia
+1 -3
View File
@@ -3,7 +3,6 @@
#include <cstdlib>
#include <iostream>
#include <memory>
#include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp>
#include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.hpp>
@@ -47,13 +46,12 @@ class Application {
ResourceRegistryRef resources();
PanelRegistry& panels() { return this->panelRegistry; }
SceneRegistry& scenes() { return this->sceneRegistry; }
SceneViewRegistry& views() { return this->sceneViewRegistry; }
SceneViewRegistry& sceneViews() { return this->sceneViewRegistry; }
UI& ui() { return this->_ui; }
DebugUI& debugUI() { return this->_debugUI; }
Canvas canvas;
Camera2D camera;
std::vector<std::string> args;
ConsoleCommandRegistry consoleCommandRegistry;
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <glm/gtc/matrix_transform.hpp>
#include <sophia/math.hpp>
namespace sophia {
class Camera2D {
public:
glm::vec2 position{0.f, 0.f};
void setProjection(float left, float right, float bottom, float top) {
projection = glm::ortho(left, right, bottom, top);
}
const glm::mat4& getProjection() const { return projection; }
glm::mat4 getView() const {
return glm::translate(glm::mat4(1.f), glm::vec3(-position, 0.f));
}
glm::mat4 getViewProjection() const { return projection * getView(); }
private:
glm::mat4 projection{1.f};
};
} // namespace sophia
+67 -6
View File
@@ -43,15 +43,55 @@ struct TransformComponent {
}
};
/**
* Draws a mesh, placed by the entity's TransformComponent. Opaque and unlit
* for now.
*/
struct MeshComponent {
MeshHandle handle;
// Multiplied with the mesh's vertex colors. Alpha is ignored. Stands in for
// a material until there are materials.
glm::vec4 color{1.f, 1.f, 1.f, 1.f};
};
/**
* A textured quad in the XY plane, placed by the entity's TransformComponent.
* Only rotation around Z is meaningful.
*/
struct SpriteComponent {
// Invalid draws a solid quad in `color`.
TextureHandle texture = INVALID_HANDLE;
glm::vec4 color{1.f, 1.f, 1.f, 1.f};
// Size in world units, before the transform's scale.
glm::vec2 size{1.f, 1.f};
// Point on the sprite, from (0, 0) at its bottom-left to (1, 1) at its
// top-right, that sits at the transform's position and that it rotates
// around.
glm::vec2 pivot{0.5f, 0.5f};
// Region of the texture to draw, as normalized (u0, v0, u1, v1) with
// (0, 0) at the texture's top-left.
glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f};
// Higher draws on top, whatever the depth. Sprites with the same sortOrder
// draw back to front from the camera; the transform's position.z is depth
// like any other axis, not a layer.
s32 sortOrder = 0;
// Maps the unit quad (0, 0)-(1, 1) to world space: to the sprite's
// rectangle around its pivot, then by `transform`.
glm::mat4 getModel(const TransformComponent& transform) const {
return transform.mat4() *
glm::translate(glm::mat4{1.f}, glm::vec3{-pivot * size, 0.f}) *
glm::scale(glm::mat4{1.f}, glm::vec3{size, 1.f});
}
};
struct CameraComponent {
struct Orthographic {
float height = 10.f;
float nearPlane = -1.f;
float farPlane = 1.f;
float nearPlane = -1000.f;
float farPlane = 1000.f;
};
struct Perspective {
@@ -66,17 +106,38 @@ struct CameraComponent {
Orthographic ortho;
Perspective perspective;
// View -> clip space. World +Y is up on screen: Vulkan's clip space has +Y
// pointing down, so the projection flips it.
glm::mat4 getProjection(float aspect) const {
glm::mat4 result;
if (projection == Projection::Perspective) {
return glm::perspective(perspective.fov, aspect, perspective.nearPlane,
result = glm::perspective(perspective.fov, aspect, perspective.nearPlane,
perspective.farPlane);
}
} else {
float halfHeight = ortho.height / 2.f;
float halfWidth = halfHeight * aspect;
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
result = glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
ortho.nearPlane, ortho.farPlane);
}
result[1][1] *= -1.f;
return result;
}
// World -> view space for this camera placed by `transform`. Ignores the
// transform's scale; zoom is the projection's job. The camera looks down
// view-space -Z.
static glm::mat4 getView(const TransformComponent& transform) {
glm::mat4 world = glm::translate(glm::mat4{1.f}, transform.position) *
glm::mat4_cast(transform.rotation);
return glm::inverse(world);
}
// World -> clip space for this camera placed by `transform`.
glm::mat4 getViewProjection(const TransformComponent& transform,
float aspect) const {
return getProjection(aspect) * getView(transform);
}
};
} // namespace sophia
+7
View File
@@ -59,4 +59,11 @@ class Entity {
Scene* scene = nullptr;
};
template <typename Fn>
void Scene::forEachEntity(Fn&& fn) {
for (auto [handle] : registry.storage<entt::entity>().each()) {
fn(Entity{handle, this});
}
}
} // namespace sophia
+6 -4
View File
@@ -83,7 +83,7 @@ class EventSystem {
this->eventDelegates[eventType] =
std::make_unique<std::any>(EventDelegate<E>());
log::trace("EventSystem creating new event type: ", eventType);
log::trace("EventSystem creating new event type: {}", eventType);
}
EventDelegate<E>& delegate =
@@ -102,7 +102,7 @@ class EventSystem {
this->eventDelegates[eventType] =
std::make_unique<std::any>(EventDelegate<E>());
log::trace("EventSystem creating new event type: ", eventType);
log::trace("EventSystem creating new event type: {}", eventType);
}
EventDelegate<E>& delegate =
@@ -124,12 +124,14 @@ class EventSystem {
std::any_cast<EventDelegate<E>&>(*this->eventDelegates.at(eventType));
delegate.invoke(event);
} catch (const std::out_of_range& error) {
log::error("event dispatch failed: ", event.toString());
log::error("event dispatch failed: {}", event.toString());
}
}
private:
EventSystem() { log::info("EventSystem Initialized: ", this); }
EventSystem() {
log::info("EventSystem Initialized: {}", static_cast<const void*>(this));
}
~EventSystem() { log::info("EventSystem Destroyed"); }
std::unordered_map<EventType, std::unique_ptr<std::any>> eventDelegates;
-2
View File
@@ -1,6 +1,5 @@
#pragma once
#include <sophia/camera_2d.hpp>
#include <sophia/math.hpp>
#include <sophia/types.hpp>
@@ -11,7 +10,6 @@ struct FrameInfo {
double elapsedTime;
double deltaTime;
glm::vec2 currentFramebufferExtent;
const Camera2D& camera;
};
} // namespace sophia
+2
View File
@@ -38,11 +38,13 @@ class Panel;
class RenderTarget;
class Scene;
struct SceneView;
class Texture;
using MeshHandle = Handle<Mesh>;
using PanelHandle = Handle<Panel>;
using RenderTargetHandle = Handle<RenderTarget>;
using SceneHandle = Handle<Scene>;
using SceneViewHandle = Handle<SceneView>;
using TextureHandle = Handle<Texture>;
} // namespace sophia
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <sophia/math.hpp>
#include <sophia/types.hpp>
#include <vector>
namespace sophia {
// Layout must match the per-vertex inputs in mesh.vert.
struct MeshVertex {
vec3 position{0.f, 0.f, 0.f};
vec3 normal{0.f, 0.f, 1.f};
// Texture coordinates, with (0, 0) at the texture's top-left.
vec2 uv{0.f, 0.f};
vec4 color{1.f, 1.f, 1.f, 1.f};
};
/**
* Geometry on the CPU, uploaded with `resources().create<Mesh>(data)`.
* Triangles wind counter-clockwise when seen from the front; back faces are
* culled. See primitives.hpp for ready-made shapes.
*/
struct MeshData {
std::vector<MeshVertex> vertices;
// Empty draws the vertices in order, three per triangle.
std::vector<u32> indices;
};
} // namespace sophia
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <sophia/math.hpp>
#include <sophia/mesh_data.hpp>
/**
* Ready-made shapes, centered on the origin. They return MeshData rather than
* a mesh so they can be adjusted before uploading:
*
* MeshHandle cube = resources().create<Mesh>(primitives::cube());
*/
namespace sophia::primitives {
// An axis-aligned cube `size` across. Each face has its own four vertices so
// it gets its own normal and UVs.
MeshData cube(float size = 1.f, vec4 color = vec4{1.f});
} // namespace sophia::primitives
@@ -1,8 +1,9 @@
#pragma once
#include <sophia/canvas.hpp>
#include <sophia/aabb.hpp>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/mesh_data.hpp>
#include <sophia/types.hpp>
#include <string>
#include <type_traits>
@@ -26,8 +27,14 @@ class ResourceRegistryRef {
void requestResize(RenderTargetHandle handle, u32 width, u32 height);
glm::vec2 getExtent(RenderTargetHandle handle) const;
glm::vec2 getExtent(TextureHandle handle) const;
// In the mesh's own space.
AABB getBounds(MeshHandle handle) const;
bool isAlive(MeshHandle handle) const;
bool isAlive(RenderTargetHandle handle) const;
bool isAlive(TextureHandle handle) const;
template <typename T>
void release(Handle<T> handle);
@@ -38,23 +45,30 @@ class ResourceRegistryRef {
explicit ResourceRegistryRef(ResourceRegistry& registry)
: registry{&registry} {}
MeshHandle createResource(std::type_identity<Mesh>,
std::vector<CanvasVertex> vertices,
std::vector<u32> indices = {});
MeshHandle createResource(std::type_identity<Mesh>, const MeshData& data);
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
u32 width = 1, u32 height = 1);
TextureHandle createResource(std::type_identity<Texture>, u32 width,
u32 height, std::vector<u8> pixels);
ResourceRegistry* registry;
};
template <>
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
template <>
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path);
template <>
void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
template <>
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
template <>
void ResourceRegistryRef::release<Texture>(TextureHandle handle);
} // namespace sophia
+17 -5
View File
@@ -13,13 +13,23 @@ class Scene {
Scene();
~Scene();
void createEntity(const std::string& name = {});
Entity createEntity(const std::string& name = {});
void destroyEntity(Entity entity);
// The camera a render request uses when it doesn't name one. Null until
// set.
entt::entity getActiveCamera() const { return activeCamera; }
void setActiveCamera(entt::entity camera) { activeCamera = camera; }
Entity createCamera(const std::string& name = "Camera");
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
template <typename Fn>
void forEachEntity(Fn&& fn);
Entity getActiveCamera();
void setActiveCamera(Entity camera);
bool isVisible() const { return this->visible; }
void setVisible(bool visible) { this->visible = visible; }
bool isActive() const { return this->active; }
void setActive(bool active) { this->active = active; }
private:
friend class Entity;
@@ -27,6 +37,8 @@ class Scene {
entt::registry registry;
entt::entity activeCamera = entt::null;
bool visible = true;
bool active = true;
};
} // namespace sophia
+2
View File
@@ -19,6 +19,8 @@ class SceneRegistry {
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
Scene& get(SceneHandle handle) { return pool.get(handle); }
const Scene& peek(SceneHandle handle) const { return pool.get(handle); }
void release(SceneHandle handle) { pool.release(handle); }
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
+5 -1
View File
@@ -17,9 +17,13 @@ enum class ClearFlags : u8 {
struct SceneView {
SceneHandle scene = INVALID_HANDLE;
RenderTargetHandle target = INVALID_HANDLE;
RenderTargetHandle target =
INVALID_HANDLE; // null implies render to swapchain
// Null implies the scene's active camera. May belong to another scene,
// e.g. an editor camera that isn't part of the scene being shown.
Entity camera;
// TODO(#76): not used yet; every view renders to its whole target.
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
vec4 clearColor{0.f, 0.f, 0.f, 1.f};
@@ -35,6 +35,10 @@ class SceneViewRegistry {
return pool.get(handle);
}
const SceneView& peek(SceneViewHandle handle) const {
return pool.get(handle);
}
void release(SceneViewHandle handle) {
isRenderQueueDirty = true;
pool.release(handle);
+2
View File
@@ -48,6 +48,7 @@ class Panel {
bool isLocked() const { return config.locked; }
bool isVisible() const { return this->visible; }
bool isShown() const { return this->visible && this->shown; }
void setVisible(bool visible) { this->visible = visible; }
void toggleVisible() { this->visible = !this->visible; }
@@ -55,6 +56,7 @@ class Panel {
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
bool visible = true;
bool shown = false;
private:
Config config;
+1 -1
View File
@@ -38,7 +38,7 @@ class PanelRegistry {
const T& get(PanelHandle handle) const {
const Panel& panel = get(handle);
SOPHIA_ASSERT(dynamic_cast<const T*>(&panel) != nullptr,
"PanelHandle does not refer to a", typeid(T).name());
"PanelHandle does not refer to a {}", typeid(T).name());
return static_cast<const T&>(panel);
}
+7 -3
View File
@@ -1,7 +1,9 @@
/**
* Usage:
* SOPHIA_ASSERT(pipelineLayout != nullptr);
* SOPHIA_ASSERT(index < count, "index", index, "out of bounds:", count);
* SOPHIA_ASSERT(index < count, "index {} out of bounds: {}", index, count);
*
* The optional message is a std::format string, checked at compile time.
*
* Compiled out entirely in release (NDEBUG) builds, matching <cassert>'s
* assert() semantics: don't rely on side effects inside the condition.
@@ -10,14 +12,16 @@
#pragma once
#include <cstdlib>
#include <format>
#include <sophia/util/logger.hpp>
#ifndef NDEBUG
#define SOPHIA_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
::sophia::log::fatal("Assertion failed:", #condition, "at", __FILE__, \
":", __LINE__ __VA_OPT__(, ) __VA_ARGS__); \
::sophia::log::fatal( \
"Assertion failed: {} at {}:{}" __VA_OPT__(" - {}"), #condition, \
__FILE__, __LINE__ __VA_OPT__(, ::std::format(__VA_ARGS__))); \
std::abort(); \
} \
} while (false)
+8 -5
View File
@@ -2,13 +2,12 @@
#include <functional>
#include <memory>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include <string>
#include <typeinfo>
#include <vector>
#include <sophia/util/logger.hpp>
#define DEFAULT_DELEGATE_FUNCTOR_ALLOC 5
namespace sophia {
@@ -39,7 +38,9 @@ class expired_weak_object : public std::runtime_error {
template <class O, typename... A>
std::function<void(A...)> createWeakFunction(void (O::*method)(A...),
std::shared_ptr<O> obj) {
if (!obj) { throw std::invalid_argument("method and obj must not be null"); }
if (!obj) {
throw std::invalid_argument("method and obj must not be null");
}
std::weak_ptr<O> weakObj = obj;
@@ -82,7 +83,7 @@ class Delegate {
try {
this->functors[i](std::forward<A>(args)...);
} catch (const expired_weak_object& e) {
log::error("delegate invoke error: ", e.what());
log::error("delegate invoke error: {}", e.what());
deadFunctorIndexs.push_back(i);
}
}
@@ -97,7 +98,9 @@ class Delegate {
void operator=(Functor c) {
clear();
if (c != nullptr) { add(c); }
if (c != nullptr) {
add(c);
}
}
void operator+=(Functor c) { add(c); }
+94 -65
View File
@@ -1,22 +1,36 @@
/**
* Lots of work still needs to be done in here.
* std::format-style logging. Format strings are checked at compile time.
*
* Usage:
* using namespace sophia;
* log::fatal("fatal message")
* log::info("Infomation message")
* log::fatal("failed to load {}", path)
* log::info("loaded {} meshes in {:.2f}ms", count, ms)
* log::debug("value: {}", x) // prefixed with file:line
* log::print("no prefix, just the message")
*
* Everything is compiled out in release (NDEBUG) builds.
*
* TODO Impl some form of log error handling
*/
#pragma once
#include <iostream>
#include <sophia/types.hpp>
#include <cstdio>
#include <format>
#include <print>
#include <source_location>
#include <string_view>
#include <type_traits>
#include <utility>
namespace sophia::log {
#ifndef NDEBUG
inline constexpr bool enabled = true;
#else
inline constexpr bool enabled = false;
#endif // !NDEBUG
enum class LEVEL {
NONE,
FATAL,
@@ -28,98 +42,113 @@ enum class LEVEL {
TRACE,
};
// Helper print functions
#define LOG_PRINT_HELPER(MSG) std::cout << MSG
#define LOG_PREFIX_FATAL LOG_PRINT_HELPER("\033[1;91m[FATAL]\033[0m ")
#define LOG_PREFIX_ERROR LOG_PRINT_HELPER("\033[1;31m[ERROR]\033[0m ")
#define LOG_PREFIX_WARNING LOG_PRINT_HELPER("\033[1;33m[WARNING]\033[0m ")
#define LOG_PREFIX_INFO LOG_PRINT_HELPER("\033[32m[INFO]\033[0m ")
#define LOG_PREFIX_VERBOSE LOG_PRINT_HELPER("\033[36m[VERBOSE]\033[0m ")
#define LOG_PREFIX_DEBUG LOG_PRINT_HELPER("\033[1;34m[DEBUG]\033[0m ")
#define LOG_PREFIX_TRACE LOG_PRINT_HELPER("\033[1;90m[TRACE]\033[0m ")
namespace detail {
template <typename... Args>
void log(LEVEL level = LEVEL::NONE, Args... args) {
#ifndef NDEBUG
constexpr std::string_view prefix(LEVEL level) {
switch (level) {
case LEVEL::FATAL:
LOG_PREFIX_FATAL;
break;
return "\033[1;91m[FATAL]\033[0m ";
case LEVEL::ERROR:
LOG_PREFIX_ERROR;
break;
return "\033[1;31m[ERROR]\033[0m ";
case LEVEL::WARNING:
LOG_PREFIX_WARNING;
break;
return "\033[1;33m[WARNING]\033[0m ";
case LEVEL::INFO:
LOG_PREFIX_INFO;
break;
return "\033[32m[INFO]\033[0m ";
case LEVEL::VERBOSE:
LOG_PREFIX_VERBOSE;
break;
// case LEVEL::DEBUGGING:
// LOG_PREFIX_DEBUG;
// break;
return "\033[36m[VERBOSE]\033[0m ";
case LEVEL::DEBUGGING:
return "\033[1;34m[DEBUG]\033[0m ";
case LEVEL::TRACE:
LOG_PREFIX_TRACE;
break;
return "\033[1;90m[TRACE]\033[0m ";
default:
std::cout << "\033[1;90m[LOG]\033[0m ";
break;
return "\033[1;90m[LOG]\033[0m ";
}
}
((std::cout << ' ' << std::forward<Args>(args)), ...);
std::cout << std::endl;
#else
(void)level;
(void)std::initializer_list<int>{((void)args, 0)...};
#endif // !NDEBUG
inline void write(std::string_view prefix, std::string_view message) {
std::println(stdout, "{}{}", prefix, message);
std::fflush(stdout);
}
// Captures the caller's location alongside the format string, since a
// defaulted std::source_location parameter can't follow a parameter pack.
template <typename... Args>
struct FormatWithLocation {
std::format_string<Args...> fmt;
std::source_location location;
template <typename S>
requires std::is_convertible_v<const S&, std::string_view>
consteval FormatWithLocation(
const S& fmt,
std::source_location location = std::source_location::current())
: fmt(fmt), location(location) {}
};
} // namespace detail
template <typename... Args>
void log(LEVEL level, std::format_string<Args...> fmt, Args&&... args) {
if constexpr (enabled) {
detail::write(detail::prefix(level),
std::format(fmt, std::forward<Args>(args)...));
}
}
template <typename... Args>
void fatal(Args... args) {
log(LEVEL::FATAL, args...);
void fatal(std::format_string<Args...> fmt, Args&&... args) {
log(LEVEL::FATAL, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void error(Args... args) {
log(LEVEL::ERROR, args...);
void error(std::format_string<Args...> fmt, Args&&... args) {
log(LEVEL::ERROR, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void warning(Args... args) {
log(LEVEL::WARNING, args...);
void warning(std::format_string<Args...> fmt, Args&&... args) {
log(LEVEL::WARNING, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void info(Args... args) {
log(LEVEL::INFO, args...);
void info(std::format_string<Args...> fmt, Args&&... args) {
log(LEVEL::INFO, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void verbose(Args... args) {
log(LEVEL::VERBOSE, args...);
void verbose(std::format_string<Args...> fmt, Args&&... args) {
log(LEVEL::VERBOSE, fmt, std::forward<Args>(args)...);
}
template <typename... Args>
void debug(const char* file, u16 line, Args... args) {
#ifndef NDEBUG
LOG_PREFIX_DEBUG;
std::cout << file << "::" << line;
((std::cout << ' ' << std::forward<Args>(args)), ...);
std::cout << std::endl;
#else
(void)file;
(void)line;
(void)std::initializer_list<int>{((void)args, 0)...};
#endif // !NDEBUG
void debug(detail::FormatWithLocation<std::type_identity_t<Args>...> fmt,
Args&&... args) {
if constexpr (enabled) {
detail::write(
detail::prefix(LEVEL::DEBUGGING),
std::format("{}:{} {}", fmt.location.file_name(), fmt.location.line(),
std::format(fmt.fmt, std::forward<Args>(args)...)));
}
}
// trick
#define debug(...) debug(__FILE__, __LINE__, __VA_ARGS__)
template <typename... Args>
void trace(Args... args) {
log(LEVEL::TRACE, args...);
void trace(std::format_string<Args...> fmt, Args&&... args) {
log(LEVEL::TRACE, fmt, std::forward<Args>(args)...);
}
// Like the functions above, but with no "[LEVEL]" prefix. With no arguments,
// an empty line.
inline void print() {
if constexpr (enabled) {
detail::write("", "");
}
}
template <typename... Args>
void print(std::format_string<Args...> fmt, Args&&... args) {
if constexpr (enabled) {
detail::write("", std::format(fmt, std::forward<Args>(args)...));
}
}
} // namespace sophia::log
+4 -7
View File
@@ -111,15 +111,12 @@ class Pool {
};
const Slot& validate(Handle<T> handle) const {
SOPHIA_ASSERT(handle.isValid(), "invalid handle:", name);
SOPHIA_ASSERT(handle.index < slots.size(), name,
"handle index out of range:", handle.index);
SOPHIA_ASSERT(handle.isValid(), "invalid handle: {}", name);
SOPHIA_ASSERT(handle.index < slots.size(), "{} handle index out of range: {}", name, static_cast<u32>(handle.index));
const Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation, "stale", name,
"handle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.item != nullptr, name,
"handle refers to a released item:", handle.index);
SOPHIA_ASSERT(slot.generation == handle.generation, "stale {} handle (generation mismatch): {}", name, static_cast<u32>(handle.index));
SOPHIA_ASSERT(slot.item != nullptr, "{} handle refers to a released item: {}", name, static_cast<u32>(handle.index));
return slot;
}
+114 -46
View File
@@ -5,19 +5,23 @@
#include <chrono>
#include <cstddef>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <map>
#include <optional>
#include <set>
#include <sophia/application.hpp>
#include <sophia/util/logger.hpp>
#include <string>
#include <tracy/Tracy.hpp>
#include <utility>
#include <vector>
#include <vulkan/vulkan.hpp>
#include "rendering/imgui_renderer.hpp"
#include "rendering/model_render_system.hpp"
#include "rendering/render_context.hpp"
#include "rendering/render_graph.hpp"
#include "rendering/scene_renderer.hpp"
#include "renderer/imgui_renderer.hpp"
#include "renderer/model_render_system.hpp"
#include "renderer/render_context.hpp"
#include "renderer/render_graph.hpp"
#include "renderer/scene_renderer.hpp"
#include "resources/mesh.hpp"
#include "resources/render_target.hpp"
#include "resources/resource_registry.hpp"
@@ -31,17 +35,6 @@ namespace sophia {
namespace {
struct Vertex {
glm::vec2 position;
glm::vec3 color;
};
struct PushConstants {
glm::mat4 transform;
glm::vec4 color;
glm::vec4 data;
};
std::vector<std::string> commandLineArgs;
constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f};
@@ -65,7 +58,8 @@ class Application::Impl {
void run() {
ModelRenderSystem modelRenderSystem{device, renderContext};
SceneRenderer sceneRenderer{this->application.sceneRegistry,
this->resourceRegistry};
this->resourceRegistry, device,
renderContext.getImageFormat()};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount(),
this->resourceRegistry};
@@ -86,11 +80,13 @@ class Application::Impl {
PanelRegistry& panelRegistry = this->application.panelRegistry;
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
const SceneViewRegistry& sceneViewRegistryConst =
this->application.sceneViewRegistry;
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
// Views already warned about, so each warns once.
std::set<SceneViewHandle> viewsWithoutCamera;
std::set<SceneViewHandle> viewsOverwritingOthers;
auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = startTime;
u32 frameIndex = 0;
@@ -113,11 +109,17 @@ class Application::Impl {
vk::Extent2D extent = renderContext.getExtent();
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
glm::vec2(static_cast<float>(extent.width),
static_cast<float>(extent.height)),
this->application.camera};
static_cast<float>(extent.height))};
{
ZoneScopedN("Application::onUpdate");
this->application.onUpdate(frameInfo);
this->application.camera.setProjection(
-frameInfo.currentFramebufferExtent.x / 2.f,
}
// Canvas draws in window pixels, origin at the center.
// TODO: replace with Debug/Gizmos drawn into scene views through their
// cameras.
glm::mat4 canvasProjection =
glm::ortho(-frameInfo.currentFramebufferExtent.x / 2.f,
frameInfo.currentFramebufferExtent.x / 2.f,
-frameInfo.currentFramebufferExtent.y / 2.f,
frameInfo.currentFramebufferExtent.y / 2.f);
@@ -128,9 +130,13 @@ class Application::Impl {
}
imguiRenderer.beginFrame();
sceneRenderer.beginFrame(renderContext.getFrameIndex());
panelRegistry.update(frameInfo);
{
ZoneScopedN("Application::onRender");
this->application.onRender();
}
std::vector<ModelRenderSystem::MeshInstance> instances;
instances.reserve(this->meshes.size() + this->drawCalls.size());
@@ -141,54 +147,114 @@ class Application::Impl {
this->drawCalls.end());
this->drawCalls.clear();
RenderGraph graph;
RenderGraph graph{renderContext.getTracyContext()};
ImageRef backbuffer =
graph.importImage(renderContext.getCurrentImage(), Access::Present);
std::map<RenderTargetHandle, ImageRef> viewTargets;
for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) {
const SceneView& view = sceneViewRegistryConst.get(handle);
struct ViewTarget {
ImageRef color;
ImageRef depth;
};
std::map<RenderTargetHandle, ViewTarget> viewTargets;
// Views without a target render straight into the backbuffer.
std::optional<ViewTarget> swapchainTarget;
for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) {
const SceneView& view = sceneViewRegistry.peek(handle);
bool toSwapchain = !view.target.isValid();
if (!sceneRegistry.isAlive(view.scene) ||
!renderTargetPool.isAlive(view.target)) {
!sceneRegistry.get(view.scene).isVisible() ||
(!toSwapchain && !renderTargetPool.isAlive(view.target))) {
continue;
}
ViewTarget target;
vk::Extent2D targetExtent;
// Whether this is the first view this frame to render to its target.
bool firstOnTarget = false;
if (toSwapchain) {
firstOnTarget = !swapchainTarget;
if (!swapchainTarget) {
swapchainTarget = {
backbuffer,
graph.importImage(renderContext.getCurrentDepthImage(),
Access::DepthWrite)};
}
target = *swapchainTarget;
targetExtent = renderContext.getExtent();
} else {
RenderTarget& renderTarget = renderTargetPool.get(view.target);
auto [it, inserted] = viewTargets.try_emplace(view.target);
firstOnTarget = inserted;
if (inserted) {
it->second =
graph.importImage(renderTargetPool.get(view.target).getImage(),
Access::SampledRead);
it->second = {
graph.importImage(renderTarget.getImage(), Access::SampledRead),
graph.importImage(renderTarget.getDepthImage(),
Access::DepthWrite)};
}
target = it->second;
targetExtent = renderTarget.getExtent();
}
if (!firstOnTarget && hasFlag(view.clearFlags, ClearFlags::Color) &&
viewsOverwritingOthers.insert(handle).second) {
log::warning(
"scene view clears its target after another view already "
"rendered to it this frame, hiding that view (give it clearFlags "
"without Color, or render it first with a lower priority)");
}
const vec4& color = view.clearColor;
graph.addPass(
"scene view",
"scene view " + std::to_string(handle.index),
[&](RenderGraph::PassBuilder& pass) {
pass.writeColor(it->second,
pass.writeColor(target.color,
hasFlag(view.clearFlags, ClearFlags::Color)
? vk::AttachmentLoadOp::eClear
: vk::AttachmentLoadOp::eLoad,
{color.r, color.g, color.b, color.a});
pass.writeDepth(target.depth,
hasFlag(view.clearFlags, ClearFlags::Depth)
? vk::AttachmentLoadOp::eClear
: vk::AttachmentLoadOp::eLoad);
},
[&sceneRenderer, &sceneViewRegistry, handle](vk::CommandBuffer) {
const SceneView& view = sceneViewRegistry.get(handle);
if (view.camera) {
sceneRenderer.render(view);
[&sceneRenderer, &sceneRegistry, &sceneViewRegistry,
&viewsWithoutCamera, handle,
targetExtent](vk::CommandBuffer commandBuffer) {
ZoneScopedN("scene view pass");
const SceneView& view = sceneViewRegistry.peek(handle);
Entity camera =
view.camera ? view.camera
: sceneRegistry.get(view.scene).getActiveCamera();
// No camera to look through: the target is just cleared.
if (!camera) {
if (viewsWithoutCamera.insert(handle).second) {
log::warning(
"scene view has no camera and its scene has no active "
"camera; it will only be cleared (see "
"Scene::createCamera)");
}
return;
}
sceneRenderer.render(commandBuffer, view, camera, targetExtent);
});
}
graph.addPass(
"main",
[&](RenderGraph::PassBuilder& pass) {
pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear,
// Keep what scene views drew into the backbuffer.
pass.writeColor(backbuffer,
swapchainTarget ? vk::AttachmentLoadOp::eLoad
: vk::AttachmentLoadOp::eClear,
CLEAR_COLOR);
for (const auto& [handle, target] : viewTargets) {
pass.sample(target);
pass.sample(target.color);
}
},
[&](vk::CommandBuffer commandBuffer) {
modelRenderSystem.render(commandBuffer, instances, frameInfo);
ZoneScopedN("main pass");
modelRenderSystem.render(commandBuffer, instances,
canvasProjection);
imguiRenderer.endFrame(commandBuffer);
});
@@ -196,6 +262,7 @@ class Application::Impl {
graph.execute(commandBuffer);
renderContext.endFrame();
FrameMark;
frameIndex++;
}
@@ -209,18 +276,19 @@ class Application::Impl {
auto endTime = std::chrono::high_resolution_clock::now();
double totalRuntime =
std::chrono::duration<double>(endTime - startTime).count();
log::info("Application ran for", totalRuntime, "s");
log::info("Application ran for {:.2f}s", totalRuntime);
}
void pushMesh(std::vector<CanvasVertex> vertices, std::vector<u32> indices) {
Mesh::Builder builder{};
builder.vertices.reserve(vertices.size());
MeshData data;
data.vertices.reserve(vertices.size());
for (const CanvasVertex& vertex : vertices) {
builder.vertices.push_back({vertex.position, vertex.color});
data.vertices.push_back(
{.position = {vertex.position, 0.f}, .color = {vertex.color, 1.f}});
}
builder.indices = std::move(indices);
data.indices = std::move(indices);
this->meshes.push_back(std::make_unique<Mesh>(this->device, builder));
this->meshes.push_back(std::make_unique<Mesh>(this->device, data));
}
void clearMeshes() {
@@ -1,10 +1,12 @@
#include "rendering/imgui_renderer.hpp"
#include "renderer/imgui_renderer.hpp"
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>
#include <imgui_internal.h>
#include <tracy/Tracy.hpp>
#include "resources/render_target.hpp"
namespace sophia {
@@ -79,6 +81,7 @@ ImGuiRenderer::~ImGuiRenderer() {
}
void ImGuiRenderer::beginFrame() {
ZoneScoped;
releaseStaleTextures();
ImGui_ImplVulkan_NewFrame();
@@ -91,12 +94,13 @@ void ImGuiRenderer::beginFrame() {
}
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
vk::ImageView view =
resources.renderTargetPool.get(handle).getImage().getView();
const RenderTarget& target = resources.renderTargetPool.get(handle);
vk::ImageView view = target.getImage().getView();
auto it = textures.find(handle.index);
if (it != textures.end()) {
if (it->second.generation == handle.generation && it->second.view == view) {
if (it->second.generation == handle.generation &&
it->second.version == target.getVersion()) {
return (ImTextureID)it->second.descriptorSet;
}
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
@@ -107,7 +111,8 @@ ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
// left in once the scene pass has finished.
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
textures[handle.index] = {handle.generation, view, descriptorSet};
textures[handle.index] = {handle.generation, target.getVersion(),
descriptorSet};
return (ImTextureID)descriptorSet;
}
@@ -120,7 +125,7 @@ void ImGuiRenderer::releaseStaleTextures() {
bool stale =
!renderTargetPool.isAlive(handle) ||
renderTargetPool.get(handle).getImage().getView() != it->second.view;
renderTargetPool.get(handle).getVersion() != it->second.version;
if (stale) {
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
it = textures.erase(it);
@@ -181,6 +186,7 @@ void ImGuiRenderer::applyLayoutIfNeeded(ImGuiID dockspaceId,
}
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
ZoneScoped;
ImGui::Render();
ImDrawData* mainDrawData = ImGui::GetDrawData();
@@ -37,7 +37,7 @@ class ImGuiRenderer {
struct Texture {
u32 generation;
vk::ImageView view;
u32 version;
VkDescriptorSet descriptorSet;
};
+154
View File
@@ -0,0 +1,154 @@
#include "renderer/mesh_render_system.hpp"
#include <algorithm>
#include <sophia/util/logger.hpp>
#include <tracy/Tracy.hpp>
#include "shaders/mesh.frag.spv.h"
#include "shaders/mesh.vert.spv.h"
namespace sophia {
namespace {
// Mesh::getAttributeDescriptions() takes locations below this.
constexpr u32 FIRST_INSTANCE_LOCATION = 4;
} // namespace
MeshRenderSystem::MeshRenderSystem(Device& device, vk::Format colorFormat,
vk::Format depthFormat,
ResourceRegistry& resources)
: device{device}, resources{resources} {
createPipelineLayout();
createPipeline(colorFormat, depthFormat);
createInstanceBuffers();
}
MeshRenderSystem::~MeshRenderSystem() {
log::trace("destroyed sophia::MeshRenderSystem");
}
void MeshRenderSystem::beginFrame(u32 frameIndex) {
this->frameIndex = frameIndex;
this->instanceCursor = 0;
}
void MeshRenderSystem::render(vk::CommandBuffer commandBuffer,
const glm::mat4& viewProjection,
std::span<const Instance> instances,
std::span<const Batch> batches) {
ZoneScoped;
if (instances.empty()) {
return;
}
u32 available = MAX_INSTANCES_PER_FRAME - this->instanceCursor;
if (instances.size() > available) {
log::warning(
"MeshRenderSystem: frame instance buffer full, dropping {} meshes",
instances.size() - available);
}
Buffer& buffer = *this->instanceBuffers[this->frameIndex];
u32 base = this->instanceCursor;
u32 count = std::min(static_cast<u32>(instances.size()), available);
buffer.writeToBuffer(const_cast<Instance*>(instances.data()),
count * sizeof(Instance), base * sizeof(Instance));
this->instanceCursor += count;
this->graphicsPipeline->bind(commandBuffer);
PushConstantData push{viewProjection};
commandBuffer.pushConstants(this->pipelineLayout.get(),
vk::ShaderStageFlagBits::eVertex, 0,
sizeof(PushConstantData), &push);
// Binding 1 stays bound across batches; each mesh rebinds binding 0.
vk::Buffer instanceBuffer = buffer.getBuffer();
vk::DeviceSize offset = 0;
commandBuffer.bindVertexBuffers(1, 1, &instanceBuffer, &offset);
for (const Batch& batch : batches) {
if (batch.firstInstance >= count) {
break;
}
u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance);
const Mesh& mesh = this->resources.meshPool.get(batch.mesh);
mesh.bind(commandBuffer);
mesh.draw(commandBuffer, batchCount, base + batch.firstInstance);
}
}
void MeshRenderSystem::createPipelineLayout() {
vk::PushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
try {
this->pipelineLayout =
this->device.get()->createPipelineLayoutUnique(pipelineLayoutInfo);
log::trace("created vk::PipelineLayout");
} catch (const vk::SystemError& err) {
log::fatal("failed to create vk::PipelineLayout");
throw std::runtime_error("failed to create vk::PipelineLayout");
}
}
void MeshRenderSystem::createPipeline(vk::Format colorFormat,
vk::Format depthFormat) {
auto vertexShader = Shader::Builder()
.fromSPIRV(shaders::mesh_vert_spv)
.setStage(ShaderStage::VERTEX)
.build();
auto fragmentShader = Shader::Builder()
.fromSPIRV(shaders::mesh_frag_spv)
.setStage(ShaderStage::FRAGMENT)
.build();
std::vector<vk::VertexInputBindingDescription> bindings =
Mesh::getBindingDescriptions();
bindings.push_back({1, sizeof(Instance), vk::VertexInputRate::eInstance});
std::vector<vk::VertexInputAttributeDescription> attributes =
Mesh::getAttributeDescriptions();
// A mat4 attribute takes four consecutive locations, one per column.
for (u32 column = 0; column < 4; column++) {
attributes.push_back({FIRST_INSTANCE_LOCATION + column, 1,
vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, model) +
column * sizeof(glm::vec4))});
}
attributes.push_back({FIRST_INSTANCE_LOCATION + 4, 1,
vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, color))});
this->graphicsPipeline = GraphicsPipeline::Builder(this->device)
.setVertexShader(*vertexShader)
.setFragmentShader(*fragmentShader)
.setPipelineLayout(this->pipelineLayout.get())
.setColorAttachmentFormats({colorFormat})
.setDepthAttachmentFormat(depthFormat)
.setVertexInput(bindings, attributes)
.setDepthTest(true, true)
.setCullMode(vk::CullModeFlagBits::eBack)
.build();
}
void MeshRenderSystem::createInstanceBuffers() {
for (std::unique_ptr<Buffer>& buffer : this->instanceBuffers) {
buffer = std::make_unique<Buffer>(
this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME,
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
buffer->map();
}
}
} // namespace sophia
@@ -0,0 +1,80 @@
#pragma once
#include <array>
#include <memory>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <span>
#include "renderer/render_context.hpp"
#include "resources/resource_registry.hpp"
#include "vulkan/buffer.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
namespace sophia {
/**
* Draws batches of opaque meshes, depth tested and back-face culled. Knows
* nothing about scenes: callers hand it instances grouped into runs that
* share a mesh.
*
* Unlit for now: each pixel is the vertex color times the instance color.
* Normals and UVs reach the vertex shader but aren't used yet.
*
* Instances are written to a per-frame host-visible buffer, so every
* render() call within one frame appends to it; call beginFrame() once per
* frame before any render().
*/
class MeshRenderSystem {
public:
// Layout must match the per-instance inputs in mesh.vert.
struct Instance {
glm::mat4 model{1.f};
glm::vec4 color{1.f};
};
// A run of consecutive instances of the same mesh.
struct Batch {
MeshHandle mesh;
u32 firstInstance;
u32 instanceCount;
};
static constexpr u32 MAX_INSTANCES_PER_FRAME = 32768;
MeshRenderSystem(const MeshRenderSystem&) = delete;
MeshRenderSystem& operator=(const MeshRenderSystem&) = delete;
MeshRenderSystem(Device& device, vk::Format colorFormat,
vk::Format depthFormat, ResourceRegistry& resources);
~MeshRenderSystem();
void beginFrame(u32 frameIndex);
void render(vk::CommandBuffer commandBuffer, const glm::mat4& viewProjection,
std::span<const Instance> instances,
std::span<const Batch> batches);
private:
struct PushConstantData {
glm::mat4 viewProjection{1.f};
};
void createPipelineLayout();
void createPipeline(vk::Format colorFormat, vk::Format depthFormat);
void createInstanceBuffers();
Device& device;
ResourceRegistry& resources;
vk::UniquePipelineLayout pipelineLayout;
std::unique_ptr<GraphicsPipeline> graphicsPipeline;
std::array<std::unique_ptr<Buffer>, RenderContext::MAX_FRAMES_IN_FLIGHT>
instanceBuffers;
u32 frameIndex = 0;
u32 instanceCursor = 0;
};
} // namespace sophia
@@ -1,6 +1,7 @@
#include "rendering/model_render_system.hpp"
#include "renderer/model_render_system.hpp"
#include <sophia/util/assert.hpp>
#include <tracy/Tracy.hpp>
#include "shaders/model_render_system.frag.spv.h"
#include "shaders/model_render_system.vert.spv.h"
@@ -24,8 +25,8 @@ ModelRenderSystem::ModelRenderSystem(Device& device,
.setStage(ShaderStage::FRAGMENT)
.build();
auto bindingDescriptions = Mesh::Vertex::getBindingDescriptions();
auto attributeDescriptions = Mesh::Vertex::getAttributeDescriptions();
auto bindingDescriptions = Mesh::getBindingDescriptions();
auto attributeDescriptions = Mesh::getAttributeDescriptions();
this->graphicsPipeline =
GraphicsPipeline::Builder(device)
@@ -43,9 +44,8 @@ ModelRenderSystem::~ModelRenderSystem() {
void ModelRenderSystem::render(vk::CommandBuffer commandBuffer,
std::span<const MeshInstance> instances,
const FrameInfo& frameInfo) {
glm::mat4 viewProjection = frameInfo.camera.getViewProjection();
const glm::mat4& viewProjection) {
ZoneScoped;
this->graphicsPipeline->bind(commandBuffer);
for (const MeshInstance& instance : instances) {
@@ -1,10 +1,10 @@
#pragma once
#include <memory>
#include <sophia/frame_info.hpp>
#include <sophia/math.hpp>
#include <span>
#include "rendering/render_context.hpp"
#include "renderer/render_context.hpp"
#include "resources/mesh.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
@@ -32,7 +32,7 @@ class ModelRenderSystem {
void render(vk::CommandBuffer commandBuffer,
std::span<const MeshInstance> instances,
const FrameInfo& frameInfo);
const glm::mat4& viewProjection);
private:
void createPipelineLayout();
@@ -1,9 +1,10 @@
#include "rendering/render_context.hpp"
#include "renderer/render_context.hpp"
#include <limits>
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include <tracy/Tracy.hpp>
#include "vulkan/access.hpp"
@@ -13,22 +14,31 @@ RenderContext::RenderContext(Window& window, Device& device)
: window{window}, device{device} {
createFrames();
recreateSwapchain();
createTracyContext();
}
RenderContext::~RenderContext() { this->device.waitIdle(); }
RenderContext::~RenderContext() {
this->device.waitIdle();
TracyVkDestroy(this->tracyContext);
}
vk::CommandBuffer RenderContext::beginFrame() {
ZoneScoped;
SOPHIA_ASSERT(!this->isFrameStarted,
"Can't call beginFrame while already in progress");
FrameData& frame = this->frames[this->currentFrameIndex];
vk::Fence inFlightFence = frame.inFlight.get();
vk::Result fenceResult = this->device.get()->waitForFences(
vk::Result fenceResult;
{
ZoneScopedN("wait for frame fence");
fenceResult = this->device.get()->waitForFences(
inFlightFence, vk::True, std::numeric_limits<u64>::max());
}
if (fenceResult != vk::Result::eSuccess) {
log::error("failed to wait for fence: " + vk::to_string(fenceResult));
log::error("failed to wait for fence: {}", vk::to_string(fenceResult));
}
vk::Semaphore imageAvailable = frame.imageAvailable.get();
@@ -52,7 +62,7 @@ vk::CommandBuffer RenderContext::beginFrame() {
imageInFlight, vk::True, std::numeric_limits<u64>::max());
if (fenceResult != vk::Result::eSuccess) {
log::error("failed to wait for fence: " + vk::to_string(fenceResult));
log::error("failed to wait for fence: {}", vk::to_string(fenceResult));
}
}
@@ -69,32 +79,42 @@ vk::CommandBuffer RenderContext::beginFrame() {
throw std::runtime_error("failed to begin recording command buffer");
}
TracyVkCollect(this->tracyContext,
static_cast<VkCommandBuffer>(frame.commandBuffer));
return frame.commandBuffer;
}
void RenderContext::endFrame() {
ZoneScoped;
SOPHIA_ASSERT(this->isFrameStarted,
"Can't call endFrame while no frame is in progress");
FrameData& frame = this->frames[this->currentFrameIndex];
frame.commandBuffer.end();
vk::Semaphore waitSemaphore = frame.imageAvailable.get();
vk::Semaphore signalSemaphore =
this->renderFinishedSemaphores.at(this->currentImageIndex).get();
vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE;
vk::CommandBuffer commandBuffer = frame.commandBuffer;
vk::SubmitInfo submitInfo{};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &waitSemaphore;
submitInfo.pWaitDstStageMask = &waitStage;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &signalSemaphore;
vk::SemaphoreSubmitInfo waitInfo{};
waitInfo.semaphore = frame.imageAvailable.get();
waitInfo.stageMask = SWAPCHAIN_ACQUIRE_STAGE;
this->device.getGraphicsQueue().submit(submitInfo, frame.inFlight.get());
// Present has no pipeline stage to wait on, so signal once everything in
// the submission is done.
vk::SemaphoreSubmitInfo signalInfo{};
signalInfo.semaphore = signalSemaphore;
signalInfo.stageMask = vk::PipelineStageFlagBits2::eAllCommands;
vk::CommandBufferSubmitInfo commandBufferInfo{};
commandBufferInfo.commandBuffer = frame.commandBuffer;
vk::SubmitInfo2 submitInfo{};
submitInfo.setWaitSemaphoreInfos(waitInfo);
submitInfo.setCommandBufferInfos(commandBufferInfo);
submitInfo.setSignalSemaphoreInfos(signalInfo);
this->device.getGraphicsQueue().submit2(submitInfo, frame.inFlight.get());
vk::Result result = this->swapchain->present(
this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore);
@@ -145,6 +165,27 @@ void RenderContext::recreateSwapchain() {
this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{});
}
void RenderContext::createTracyContext() {
#ifdef TRACY_ENABLE
// Tracy records and submits a one-off command buffer to line up GPU and CPU
// clocks, and waits for it.
vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(),
vk::CommandBufferLevel::ePrimary, 1};
vk::CommandBuffer commandBuffer =
this->device.get()->allocateCommandBuffers(allocInfo).front();
this->tracyContext = TracyVkContext(
static_cast<VkPhysicalDevice>(this->device.getPhysicalDevice()),
static_cast<VkDevice>(*this->device.get()),
static_cast<VkQueue>(this->device.getGraphicsQueue()),
static_cast<VkCommandBuffer>(commandBuffer));
TracyVkContextName(this->tracyContext, "Graphics", 8);
this->device.get()->freeCommandBuffers(this->device.getCommandPool(),
commandBuffer);
#endif
}
void RenderContext::createFrames() {
vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(),
vk::CommandBufferLevel::ePrimary,
@@ -6,6 +6,9 @@
#include <vector>
#include <vulkan/vulkan.hpp>
/* AFTER VULKAN */
#include <tracy/TracyVulkan.hpp>
#include "vulkan/device.hpp"
#include "vulkan/swapchain.hpp"
#include "window.hpp"
@@ -35,10 +38,17 @@ class RenderContext {
return this->swapchain->getImage(static_cast<int>(this->currentImageIndex));
}
Image& getCurrentDepthImage() const {
return this->swapchain->getDepthImage(
static_cast<int>(this->currentImageIndex));
}
vk::Extent2D getExtent() const { return this->swapchain->getExtent(); }
vk::Format getImageFormat() const {
return this->swapchain->getImageFormat();
}
u32 getFrameIndex() const { return this->currentFrameIndex; }
TracyVkCtx getTracyContext() const { return this->tracyContext; }
u32 getImageCount() const {
return static_cast<u32>(this->swapchain->imageCount());
}
@@ -46,6 +56,7 @@ class RenderContext {
private:
void recreateSwapchain();
void createFrames();
void createTracyContext();
Window& window;
Device& device;
@@ -57,6 +68,8 @@ class RenderContext {
std::vector<vk::UniqueSemaphore> renderFinishedSemaphores;
std::vector<vk::Fence> imagesInFlight;
TracyVkCtx tracyContext = nullptr;
u32 currentFrameIndex = 0;
u32 currentImageIndex = 0;
bool isFrameStarted = false;
@@ -1,6 +1,7 @@
#include "rendering/render_graph.hpp"
#include "renderer/render_graph.hpp"
#include <sophia/util/assert.hpp>
#include <tracy/Tracy.hpp>
#include <utility>
#include "vulkan/debug_label.hpp"
@@ -24,6 +25,7 @@ void RenderGraph::addPass(std::string name, const SetupFn& setup,
}
void RenderGraph::execute(vk::CommandBuffer commandBuffer) {
ZoneScoped;
for (Pass& pass : this->passes) {
this->recordPass(commandBuffer, pass);
}
@@ -53,8 +55,6 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
pending.barriers.push_back(makeImageBarrier(
imported.image->get(), imported.image->getSubresourceRange(),
imported.state, next));
pending.srcStages |= imported.state.stage;
pending.dstStages |= next.stage;
imported.state = next;
} else {
// Same-layout read after read. Nothing to record, but a later write has
@@ -66,16 +66,15 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
void RenderGraph::flush(vk::CommandBuffer commandBuffer,
PendingBarriers& pending) {
if (pending.barriers.empty()) {
return;
}
commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {},
nullptr, nullptr, pending.barriers);
recordImageBarriers(commandBuffer, pending.barriers);
pending = PendingBarriers{};
}
void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) {
ZoneTransientN(cpuZone, pass.name.c_str(), true);
TracyVkZoneTransient(this->tracyContext, gpuZone,
static_cast<VkCommandBuffer>(commandBuffer),
pass.name.c_str(), this->tracyContext != nullptr);
beginDebugLabel(commandBuffer, pass.name.c_str());
PendingBarriers pending;
@@ -84,7 +83,7 @@ void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) {
}
this->flush(commandBuffer, pending);
if (pass.colorAttachments.empty()) {
if (pass.colorAttachments.empty() && !pass.depthAttachment) {
pass.execute(commandBuffer);
} else {
this->recordRendering(commandBuffer, pass);
@@ -97,17 +96,18 @@ void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) {
std::vector<vk::RenderingAttachmentInfo> attachments;
attachments.reserve(pass.colorAttachments.size());
vk::Extent3D imageExtent =
this->images[pass.colorAttachments.front().image.index]
.image->getExtent();
ImageRef sizedBy = pass.colorAttachments.empty()
? pass.depthAttachment->image
: pass.colorAttachments.front().image;
vk::Extent3D imageExtent = this->images[sizedBy.index].image->getExtent();
vk::Extent2D extent{imageExtent.width, imageExtent.height};
for (const ColorAttachment& color : pass.colorAttachments) {
const Image& image = *this->images[color.image.index].image;
SOPHIA_ASSERT(image.getExtent().width == extent.width &&
image.getExtent().height == extent.height,
"Pass", pass.name,
"has color attachments of different sizes");
"Pass {} has color attachments of different sizes",
pass.name);
vk::RenderingAttachmentInfo attachment{};
attachment.imageView = image.getView();
@@ -124,6 +124,24 @@ void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) {
renderingInfo.colorAttachmentCount = static_cast<u32>(attachments.size());
renderingInfo.pColorAttachments = attachments.data();
vk::RenderingAttachmentInfo depthAttachment{};
if (pass.depthAttachment) {
const Image& image = *this->images[pass.depthAttachment->image.index].image;
SOPHIA_ASSERT(image.getExtent().width == extent.width &&
image.getExtent().height == extent.height,
"Pass {} has a depth attachment of a different size",
pass.name);
depthAttachment.imageView = image.getView();
depthAttachment.imageLayout =
vk::ImageLayout::eDepthStencilAttachmentOptimal;
depthAttachment.loadOp = pass.depthAttachment->loadOp;
depthAttachment.storeOp = vk::AttachmentStoreOp::eStore;
depthAttachment.clearValue.depthStencil =
vk::ClearDepthStencilValue{pass.depthAttachment->clearDepth, 0};
renderingInfo.pDepthAttachment = &depthAttachment;
}
commandBuffer.beginRendering(renderingInfo);
vk::Viewport viewport{0.0f,
@@ -148,6 +166,15 @@ RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeColor(
return *this;
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeDepth(
ImageRef image, vk::AttachmentLoadOp loadOp, float clearDepth) {
SOPHIA_ASSERT(!this->pass.depthAttachment,
"Pass {} already has a depth attachment", this->pass.name);
this->use(image, Access::DepthWrite);
this->pass.depthAttachment = DepthAttachment{image, loadOp, clearDepth};
return *this;
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::sample(ImageRef image) {
return this->use(image, Access::SampledRead);
}
@@ -157,8 +184,8 @@ RenderGraph::PassBuilder& RenderGraph::PassBuilder::use(ImageRef image,
SOPHIA_ASSERT(image.isValid() && image.index < this->graph.images.size(),
"ImageRef doesn't belong to this graph");
for (const ImageUse& existing : this->pass.uses) {
SOPHIA_ASSERT(existing.image.index != image.index, "Pass", this->pass.name,
"uses the same image twice");
SOPHIA_ASSERT(existing.image.index != image.index,
"Pass {} uses the same image twice", this->pass.name);
}
this->pass.uses.push_back({image, access});
@@ -2,10 +2,13 @@
#include <array>
#include <functional>
#include <optional>
#include <sophia/types.hpp>
#include <string>
#include <vector>
#include <vulkan/vulkan.hpp>
// After Vulkan, which it expects to already be declared.
#include <tracy/TracyVulkan.hpp>
#include "vulkan/access.hpp"
#include "vulkan/image.hpp"
@@ -31,7 +34,7 @@ using ClearColor = std::array<float, 4>;
* buffer. Passes run in the order they were added; the graph's job is to
* work out the barriers and layout transitions between them from what each
* pass says it does to each image, and to begin/end rendering for passes that
* write color attachments.
* write color or depth attachments.
*
* Built fresh every frame:
*
@@ -53,7 +56,8 @@ class RenderGraph {
using SetupFn = std::function<void(PassBuilder&)>;
using ExecuteFn = std::function<void(vk::CommandBuffer)>;
RenderGraph() = default;
explicit RenderGraph(TracyVkCtx tracyContext = nullptr)
: tracyContext{tracyContext} {}
RenderGraph(const RenderGraph&) = delete;
RenderGraph& operator=(const RenderGraph&) = delete;
@@ -67,8 +71,8 @@ class RenderGraph {
/**
* Adds a pass. `setup` runs immediately and declares what the pass does to
* images; `execute` runs later, from execute(), with the barriers already
* recorded and (if the pass writes color) rendering begun. `name` labels
* the pass's commands in debugging tools such as RenderDoc.
* recorded and (if the pass writes color or depth) rendering begun. `name`
* labels the pass's commands in debugging tools such as RenderDoc.
*/
void addPass(std::string name, const SetupFn& setup, ExecuteFn execute);
@@ -89,10 +93,17 @@ class RenderGraph {
ClearColor clearColor;
};
struct DepthAttachment {
ImageRef image;
vk::AttachmentLoadOp loadOp;
float clearDepth;
};
struct Pass {
std::string name;
std::vector<ImageUse> uses;
std::vector<ColorAttachment> colorAttachments;
std::optional<DepthAttachment> depthAttachment;
ExecuteFn execute;
};
@@ -104,9 +115,7 @@ class RenderGraph {
};
struct PendingBarriers {
std::vector<vk::ImageMemoryBarrier> barriers;
vk::PipelineStageFlags srcStages;
vk::PipelineStageFlags dstStages;
std::vector<vk::ImageMemoryBarrier2> barriers;
};
void transitionTo(ImageRef ref, const AccessState& next,
@@ -115,6 +124,7 @@ class RenderGraph {
void recordPass(vk::CommandBuffer commandBuffer, Pass& pass);
void recordRendering(vk::CommandBuffer commandBuffer, Pass& pass);
TracyVkCtx tracyContext;
std::vector<ImportedImage> images;
std::vector<Pass> passes;
};
@@ -131,6 +141,15 @@ class RenderGraph::PassBuilder {
vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear,
ClearColor clearColor = {0.f, 0.f, 0.f, 1.f});
/**
* Renders with `image` as the depth attachment, which must be the same size
* as any color attachments. At most one per pass.
*/
PassBuilder& writeDepth(
ImageRef image,
vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear,
float clearDepth = 1.f);
/** Samples `image` in a shader. */
PassBuilder& sample(ImageRef image);
+9
View File
@@ -0,0 +1,9 @@
#include "renderer/renderer.hpp"
namespace sophia {
Renderer::Renderer() {}
Renderer::~Renderer() {}
} // namespace sophia
+91
View File
@@ -0,0 +1,91 @@
#pragma once
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/types.hpp>
#include <string>
#include <vector>
#include "vulkan/buffer.hpp"
namespace sophia {
struct DrawPacket {
/* Geometry */
// BufferHandle vertexBuffer;
// BufferHandle indexBuffer;
u32 indexCount;
u32 firstIndex;
/* Shader & state */
// PipelineHandle pipelineState; // Contains shaders, blend state, depth
// settings
/* Bindings */
// DescriptorSetHandle resourceBindings; // Textures, Uniform Buffers,
// Storage Buffers
/* Transform / Constants */
u32 pushConstantData[16];
};
struct RenderView {
mat4 view;
mat4 projection;
vec2 viewport;
RenderTargetHandle target;
};
// struct ComputePacket {
// ComputePipelineHandle pipeline;
// DescriptorSetHandle
// resourceBindings; // Includes storage buffers, uniforms, textures
// // Grid dimensions
// uint32_t groupCountX = 1;
// uint32_t groupCountY = 1;
// uint32_t groupCountZ = 1;
// // Optional push constants for dynamic per-dispatch parameters
// uint8_t pushConstants[64];
// uint32_t pushConstantSize = 0;
// };
// !TODO move to render graph
enum class RenderPassType { Graphics, Compute };
// !TODO move to render graph (???)
struct RenderPass {
std::string name;
RenderPassType type;
// Declared for automatic barrier injection & resource synchronization
std::vector<Buffer> readBuffers; // BufferHandle instead?
std::vector<Buffer> writeBuffers; // BufferHandle instead?
// Commands in this pass
// std::vector<ComputePacket> computeDispatches;
std::vector<DrawPacket> opaqueDrawPackets;
std::vector<DrawPacket> transparentDrawPackets;
};
struct RenderPacket {
RenderView view;
std::vector<RenderPass> passes;
};
class Renderer {
public:
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
Renderer();
~Renderer();
// PipelineHandle createPipeline();
private:
};
} // namespace sophia
+163
View File
@@ -0,0 +1,163 @@
#include "renderer/scene_renderer.hpp"
#include <algorithm>
#include <sophia/components.hpp>
#include <sophia/util/assert.hpp>
#include <tracy/Tracy.hpp>
#include <tuple>
namespace sophia {
SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry,
ResourceRegistry& resources, Device& device,
vk::Format colorFormat)
: sceneRegistry{sceneRegistry},
resources{resources},
meshRenderSystem{device, colorFormat,
resources.renderTargetPool.getDepthFormat(), resources},
spriteRenderSystem{device, colorFormat,
resources.renderTargetPool.getDepthFormat(),
resources} {}
void SceneRenderer::beginFrame(u32 frameIndex) {
ZoneScoped;
this->meshRenderSystem.beginFrame(frameIndex);
this->spriteRenderSystem.beginFrame(frameIndex);
}
void SceneRenderer::render(vk::CommandBuffer commandBuffer,
const SceneView& sceneView, Entity camera,
vk::Extent2D extent) {
ZoneScoped;
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
"camera does not have camera component!");
float aspect =
static_cast<float>(extent.width) / static_cast<float>(extent.height);
glm::mat4 view =
CameraComponent::getView(camera.getComponent<TransformComponent>());
glm::mat4 projection =
camera.getComponent<CameraComponent>().getProjection(aspect);
const Scene& scene = this->sceneRegistry.get(sceneView.scene);
glm::mat4 viewProjection = projection * view;
// Opaque first, so sprites blend over the meshes behind them.
renderMeshes(commandBuffer, scene, viewProjection);
renderSprites(commandBuffer, scene, view, viewProjection);
}
void SceneRenderer::renderMeshes(vk::CommandBuffer commandBuffer,
const Scene& scene,
const glm::mat4& viewProjection) {
ZoneScoped;
this->meshDrawItems.clear();
{
ZoneScopedN("meshes: gather");
auto meshes =
scene.registry.view<const TransformComponent, const MeshComponent>();
for (auto [entity, transform, mesh] : meshes.each()) {
if (!this->resources.meshPool.isAlive(mesh.handle)) {
continue;
}
this->meshDrawItems.push_back(
{mesh.handle, {transform.mat4(), mesh.color}});
}
}
{
ZoneScopedN("meshes: sort");
// Opaque, so order only matters for batching: one batch per mesh.
std::ranges::sort(this->meshDrawItems, {},
[](const MeshDrawItem& item) { return item.mesh; });
}
{
ZoneScopedN("meshes: batch");
this->meshInstances.clear();
this->meshBatches.clear();
for (const MeshDrawItem& item : this->meshDrawItems) {
auto index = static_cast<u32>(this->meshInstances.size());
this->meshInstances.push_back(item.instance);
if (this->meshBatches.empty() ||
this->meshBatches.back().mesh != item.mesh) {
this->meshBatches.push_back({item.mesh, index, 0});
}
this->meshBatches.back().instanceCount++;
}
}
TracyPlot("mesh instances", static_cast<int64_t>(this->meshInstances.size()));
TracyPlot("mesh batches", static_cast<int64_t>(this->meshBatches.size()));
this->meshRenderSystem.render(commandBuffer, viewProjection,
this->meshInstances, this->meshBatches);
}
void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
const Scene& scene, const glm::mat4& view,
const glm::mat4& viewProjection) {
ZoneScoped;
this->spriteDrawItems.clear();
{
ZoneScopedN("sprites: gather");
auto sprites =
scene.registry.view<const TransformComponent, const SpriteComponent>();
for (auto [entity, transform, sprite] : sprites.each()) {
TextureHandle texture =
this->resources.texturePool.isAlive(sprite.texture)
? sprite.texture
: this->resources.whiteTexture;
// Distance in front of the camera, which looks down view-space -Z.
float depth = -(view * glm::vec4{transform.position, 1.f}).z;
this->spriteDrawItems.push_back(
{sprite.sortOrder,
depth,
texture,
{sprite.getModel(transform), sprite.color, sprite.uvRect}});
}
}
{
ZoneScopedN("sprites: sort");
// Within a sortOrder, back to front, so nearer sprites blend over farther
// ones. Texture is the last key so that sprites whose order doesn't
// matter relative to each other still end up in as few batches as
// possible.
std::ranges::sort(
this->spriteDrawItems, {}, [](const SpriteDrawItem& item) {
return std::tuple{item.sortOrder, -item.depth, item.texture};
});
}
{
ZoneScopedN("sprites: batch");
this->spriteInstances.clear();
this->spriteBatches.clear();
for (const SpriteDrawItem& item : this->spriteDrawItems) {
auto index = static_cast<u32>(this->spriteInstances.size());
this->spriteInstances.push_back(item.instance);
if (this->spriteBatches.empty() ||
this->spriteBatches.back().texture != item.texture) {
this->spriteBatches.push_back({item.texture, index, 0});
}
this->spriteBatches.back().instanceCount++;
}
}
TracyPlot("sprite instances",
static_cast<int64_t>(this->spriteInstances.size()));
TracyPlot("sprite batches", static_cast<int64_t>(this->spriteBatches.size()));
this->spriteRenderSystem.render(commandBuffer, viewProjection,
this->spriteInstances, this->spriteBatches);
}
} // namespace sophia
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <sophia/scene_registry.hpp>
#include <sophia/scene_view.hpp>
#include <vector>
#include "renderer/mesh_render_system.hpp"
#include "renderer/sprite_render_system.hpp"
#include "resources/resource_registry.hpp"
namespace sophia {
/**
* !TEMP to be decoupled once scene batching and rendering impl
*
* Turns a scene's components into draw calls for a SceneView.
*/
class SceneRenderer {
public:
SceneRenderer(const SceneRenderer&) = delete;
SceneRenderer& operator=(const SceneRenderer&) = delete;
SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources,
Device& device, vk::Format colorFormat);
// Call once per frame, before any render().
void beginFrame(u32 frameIndex);
// Records `sceneView` as seen through `camera` (an entity of the view's
// scene with a CameraComponent) into `commandBuffer`. Rendering into the
// view's target, which is `extent` in size, must already have begun.
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView,
Entity camera, vk::Extent2D extent);
private:
struct MeshDrawItem {
MeshHandle mesh;
MeshRenderSystem::Instance instance;
};
struct SpriteDrawItem {
s32 sortOrder;
// Distance in front of the camera.
float depth;
TextureHandle texture;
SpriteRenderSystem::Instance instance;
};
void renderMeshes(vk::CommandBuffer commandBuffer, const Scene& scene,
const glm::mat4& viewProjection);
void renderSprites(vk::CommandBuffer commandBuffer, const Scene& scene,
const glm::mat4& view, const glm::mat4& viewProjection);
const SceneRegistry& sceneRegistry;
ResourceRegistry& resources;
MeshRenderSystem meshRenderSystem;
SpriteRenderSystem spriteRenderSystem;
// Reused across frames to avoid reallocating.
std::vector<MeshDrawItem> meshDrawItems;
std::vector<MeshRenderSystem::Instance> meshInstances;
std::vector<MeshRenderSystem::Batch> meshBatches;
std::vector<SpriteDrawItem> spriteDrawItems;
std::vector<SpriteRenderSystem::Instance> spriteInstances;
std::vector<SpriteRenderSystem::Batch> spriteBatches;
};
} // namespace sophia
@@ -0,0 +1,202 @@
#include "renderer/sprite_render_system.hpp"
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <tracy/Tracy.hpp>
#include "shaders/sprite.frag.spv.h"
#include "shaders/sprite.vert.spv.h"
#include "vulkan/descriptors/descriptor_writer.hpp"
namespace sophia {
namespace {
constexpr u32 MAX_TEXTURES = 1024;
constexpr u32 VERTICES_PER_SPRITE = 6;
} // namespace
SpriteRenderSystem::SpriteRenderSystem(Device& device, vk::Format colorFormat,
vk::Format depthFormat,
ResourceRegistry& resources)
: device{device}, resources{resources} {
createDescriptors();
createPipelineLayout();
createPipeline(colorFormat, depthFormat);
createInstanceBuffers();
}
SpriteRenderSystem::~SpriteRenderSystem() {
log::trace("destroyed sophia::SpriteRenderSystem");
}
void SpriteRenderSystem::beginFrame(u32 frameIndex) {
this->frameIndex = frameIndex;
this->instanceCursor = 0;
}
void SpriteRenderSystem::render(vk::CommandBuffer commandBuffer,
const glm::mat4& viewProjection,
std::span<const Instance> instances,
std::span<const Batch> batches) {
ZoneScoped;
if (instances.empty()) {
return;
}
u32 available = MAX_INSTANCES_PER_FRAME - this->instanceCursor;
if (instances.size() > available) {
log::warning(
"SpriteRenderSystem: frame instance buffer full, dropping {} sprites",
instances.size() - available);
}
Buffer& buffer = *this->instanceBuffers[this->frameIndex];
u32 base = this->instanceCursor;
u32 count = std::min(static_cast<u32>(instances.size()), available);
buffer.writeToBuffer(const_cast<Instance*>(instances.data()),
count * sizeof(Instance), base * sizeof(Instance));
this->instanceCursor += count;
this->graphicsPipeline->bind(commandBuffer);
PushConstantData push{viewProjection};
commandBuffer.pushConstants(this->pipelineLayout.get(),
vk::ShaderStageFlagBits::eVertex, 0,
sizeof(PushConstantData), &push);
vk::Buffer vertexBuffer = buffer.getBuffer();
vk::DeviceSize offset = 0;
commandBuffer.bindVertexBuffers(0, 1, &vertexBuffer, &offset);
for (const Batch& batch : batches) {
if (batch.firstInstance >= count) {
break;
}
u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance);
vk::DescriptorSet set = getDescriptorSet(batch.texture);
commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
this->pipelineLayout.get(), 0, 1, &set, 0,
nullptr);
commandBuffer.draw(VERTICES_PER_SPRITE, batchCount, 0,
base + batch.firstInstance);
}
}
vk::DescriptorSet SpriteRenderSystem::getDescriptorSet(TextureHandle handle) {
auto it = this->descriptorSets.find(handle.index);
if (it != this->descriptorSets.end() &&
it->second.generation == handle.generation) {
return it->second.set;
}
const Texture& texture = this->resources.texturePool.get(handle);
vk::DescriptorImageInfo imageInfo = texture.descriptorInfo();
DescriptorWriter writer{*this->setLayout, *this->descriptorPool};
writer.writeImage(0, &imageInfo);
if (it == this->descriptorSets.end()) {
vk::DescriptorSet set;
bool allocated = writer.build(set);
SOPHIA_ASSERT(allocated, "SpriteRenderSystem: out of texture descriptors");
this->descriptorSets.emplace(static_cast<u32>(handle.index),
CachedDescriptorSet{handle.generation, set});
return set;
}
// The slot was reused by a new texture. Releasing a texture waits for the
// device to go idle, so no in-flight frame can still be using this set.
writer.overwrite(it->second.set);
it->second.generation = handle.generation;
return it->second.set;
}
void SpriteRenderSystem::createDescriptors() {
this->setLayout =
DescriptorSetLayout::Builder(this->device)
.addBinding(0, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment)
.build();
this->descriptorPool =
DescriptorPool::Builder(this->device)
.setMaxSets(MAX_TEXTURES)
.addPoolSize(vk::DescriptorType::eCombinedImageSampler, MAX_TEXTURES)
.build();
}
void SpriteRenderSystem::createPipelineLayout() {
vk::PushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
vk::DescriptorSetLayout setLayout = this->setLayout->getDescriptorSetLayout();
vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &setLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
try {
this->pipelineLayout =
this->device.get()->createPipelineLayoutUnique(pipelineLayoutInfo);
log::trace("created vk::PipelineLayout");
} catch (const vk::SystemError& err) {
log::fatal("failed to create vk::PipelineLayout");
throw std::runtime_error("failed to create vk::PipelineLayout");
}
}
void SpriteRenderSystem::createPipeline(vk::Format colorFormat,
vk::Format depthFormat) {
auto vertexShader = Shader::Builder()
.fromSPIRV(shaders::sprite_vert_spv)
.setStage(ShaderStage::VERTEX)
.build();
auto fragmentShader = Shader::Builder()
.fromSPIRV(shaders::sprite_frag_spv)
.setStage(ShaderStage::FRAGMENT)
.build();
std::vector<vk::VertexInputBindingDescription> bindings{
{0, sizeof(Instance), vk::VertexInputRate::eInstance}};
std::vector<vk::VertexInputAttributeDescription> attributes;
// A mat4 attribute takes four consecutive locations, one per column.
for (u32 column = 0; column < 4; column++) {
attributes.push_back({column, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, model) +
column * sizeof(glm::vec4))});
}
attributes.push_back({4, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, color))});
attributes.push_back({5, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, uvRect))});
this->graphicsPipeline =
GraphicsPipeline::Builder(this->device)
.setVertexShader(*vertexShader)
.setFragmentShader(*fragmentShader)
.setPipelineLayout(this->pipelineLayout.get())
.setColorAttachmentFormats({colorFormat})
.setDepthAttachmentFormat(depthFormat)
.setVertexInput(bindings, attributes)
.setAlphaBlending()
.setDepthTest(true, false, vk::CompareOp::eLessOrEqual)
.build();
}
void SpriteRenderSystem::createInstanceBuffers() {
for (std::unique_ptr<Buffer>& buffer : this->instanceBuffers) {
buffer = std::make_unique<Buffer>(
this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME,
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
buffer->map();
}
}
} // namespace sophia
@@ -0,0 +1,96 @@
#pragma once
#include <array>
#include <memory>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <span>
#include <unordered_map>
#include "renderer/render_context.hpp"
#include "resources/resource_registry.hpp"
#include "vulkan/buffer.hpp"
#include "vulkan/descriptors/descriptor_pool.hpp"
#include "vulkan/descriptors/descriptor_set_layout.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
namespace sophia {
/**
* Draws batches of textured quads. Knows nothing about scenes: callers hand
* it instances already in draw order, grouped into runs that share a texture.
* Sprites are depth tested, so opaque geometry in front hides them, but
* don't write depth, since they blend.
*
* Instances are written to a per-frame host-visible buffer, so every
* render() call within one frame appends to it; call beginFrame() once per
* frame before any render().
*/
class SpriteRenderSystem {
public:
// Layout must match the per-instance inputs in sprite.vert.
struct Instance {
glm::mat4 model{1.f};
glm::vec4 color{1.f};
glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f};
};
// A run of consecutive instances drawn with the same texture.
struct Batch {
TextureHandle texture;
u32 firstInstance;
u32 instanceCount;
};
static constexpr u32 MAX_INSTANCES_PER_FRAME = 32768;
SpriteRenderSystem(const SpriteRenderSystem&) = delete;
SpriteRenderSystem& operator=(const SpriteRenderSystem&) = delete;
SpriteRenderSystem(Device& device, vk::Format colorFormat,
vk::Format depthFormat, ResourceRegistry& resources);
~SpriteRenderSystem();
void beginFrame(u32 frameIndex);
void render(vk::CommandBuffer commandBuffer, const glm::mat4& viewProjection,
std::span<const Instance> instances,
std::span<const Batch> batches);
private:
struct PushConstantData {
glm::mat4 viewProjection{1.f};
};
struct CachedDescriptorSet {
u32 generation;
vk::DescriptorSet set;
};
void createDescriptors();
void createPipelineLayout();
void createPipeline(vk::Format colorFormat, vk::Format depthFormat);
void createInstanceBuffers();
vk::DescriptorSet getDescriptorSet(TextureHandle handle);
Device& device;
ResourceRegistry& resources;
std::unique_ptr<DescriptorSetLayout> setLayout;
std::unique_ptr<DescriptorPool> descriptorPool;
// One set per texture slot, keyed by handle index. A generation mismatch
// means the slot was reused by a new texture, so the set is rewritten.
std::unordered_map<u32, CachedDescriptorSet> descriptorSets;
vk::UniquePipelineLayout pipelineLayout;
std::unique_ptr<GraphicsPipeline> graphicsPipeline;
std::array<std::unique_ptr<Buffer>, RenderContext::MAX_FRAMES_IN_FLIGHT>
instanceBuffers;
u32 frameIndex = 0;
u32 instanceCursor = 0;
};
} // namespace sophia
-74
View File
@@ -1,74 +0,0 @@
#include "rendering/scene_renderer.hpp"
#include <sophia/components.hpp>
#include <sophia/util/assert.hpp>
namespace sophia {
namespace {
struct PushConstantData {
glm::mat4 transform{1.f};
glm::vec4 color{1.f};
};
} // namespace
SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry,
ResourceRegistry& resources)
: sceneRegistry{sceneRegistry}, resources{resources} {}
SceneRenderer::~SceneRenderer() {}
void SceneRenderer::render(SceneView sceneView) {
auto& handle = sceneView.scene;
auto& scene = this->sceneRegistry.get(handle);
auto& camera = sceneView.camera;
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
"camera does not have camera component!");
auto& registry = scene.registry;
auto view = registry.view<MeshComponent, TransformComponent>();
for (auto entity : view) {
auto& transform = view.get<TransformComponent>(entity);
log::info(transform);
}
// Get all mesh
// For each mesh, get all entities(ES) that have a transform and that mesh
// For e in ES: add e's transform component to a list
// We now have
/*
M = mesh
T = transform
Work Batch
|- M0
| |- T0
| \- T2
|
|- M1
| |- T1
| \- T5
|
\- M2
|- T3
\- T4
*/
/*
for batch in Work Batch:
batch.mesh.bind()
for transform in batch.transforms:
commandBuffer.pushConstants(transform)
batch.mesh.draw(commandBuffer)
NOTE: Use instanced rendering?
*/
}
} // namespace sophia
-29
View File
@@ -1,29 +0,0 @@
#pragma once
#include <sophia/scene_registry.hpp>
#include <sophia/scene_view.hpp>
#include "resources/resource_registry.hpp"
namespace sophia {
/**
* !TEMP to be decoupled once scene batching and rendering impl
*/
class SceneRenderer {
public:
SceneRenderer(const SceneRenderer&) = delete;
SceneRenderer& operator=(const SceneRenderer&) = delete;
SceneRenderer(const SceneRegistry& sceneRegistry,
ResourceRegistry& resources);
~SceneRenderer();
void render(SceneView sceneView);
private:
const SceneRegistry& sceneRegistry;
ResourceRegistry& resources;
};
} // namespace sophia
+27 -28
View File
@@ -5,35 +5,32 @@
namespace sophia {
std::vector<vk::VertexInputBindingDescription>
Mesh::Vertex::getBindingDescriptions() {
std::vector<vk::VertexInputBindingDescription> bindingDescriptions(1);
bindingDescriptions[0].binding = 0;
bindingDescriptions[0].stride = sizeof(Vertex);
bindingDescriptions[0].inputRate = vk::VertexInputRate::eVertex;
return bindingDescriptions;
std::vector<vk::VertexInputBindingDescription> Mesh::getBindingDescriptions() {
return {{0, sizeof(MeshVertex), vk::VertexInputRate::eVertex}};
}
std::vector<vk::VertexInputAttributeDescription>
Mesh::Vertex::getAttributeDescriptions() {
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
attributeDescriptions.push_back(
{0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, position)});
attributeDescriptions.push_back(
{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)});
// attributeDescriptions.push_back(
// {2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal)});
// attributeDescriptions.push_back(
// {3, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, uv)});
return attributeDescriptions;
Mesh::getAttributeDescriptions() {
return {
{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(MeshVertex, position)},
{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(MeshVertex, normal)},
{2, 0, vk::Format::eR32G32Sfloat, offsetof(MeshVertex, uv)},
{3, 0, vk::Format::eR32G32B32A32Sfloat, offsetof(MeshVertex, color)},
};
}
Mesh::Mesh(Device& device, const Builder& builder) : device{device} {
createVertexBuffers(builder.vertices);
createIndexBuffers(builder.indices);
Mesh::Mesh(Device& device, const MeshData& data) : device{device} {
if (!data.vertices.empty()) {
this->bounds = {data.vertices.front().position,
data.vertices.front().position};
for (const MeshVertex& vertex : data.vertices) {
this->bounds.min = glm::min(this->bounds.min, vertex.position);
this->bounds.max = glm::max(this->bounds.max, vertex.position);
}
}
createVertexBuffers(data.vertices);
createIndexBuffers(data.indices);
}
Mesh::~Mesh() = default;
@@ -49,15 +46,17 @@ void Mesh::bind(vk::CommandBuffer commandBuffer) const {
}
}
void Mesh::draw(vk::CommandBuffer commandBuffer) const {
void Mesh::draw(vk::CommandBuffer commandBuffer, u32 instanceCount,
u32 firstInstance) const {
if (this->hasIndexBuffer) {
commandBuffer.drawIndexed(this->indexCount, 1, 0, 0, 0);
commandBuffer.drawIndexed(this->indexCount, instanceCount, 0, 0,
firstInstance);
} else {
commandBuffer.draw(this->vertexCount, 1, 0, 0);
commandBuffer.draw(this->vertexCount, instanceCount, 0, firstInstance);
}
}
void Mesh::createVertexBuffers(const std::vector<Vertex>& vertices) {
void Mesh::createVertexBuffers(const std::vector<MeshVertex>& vertices) {
this->vertexCount = static_cast<u32>(vertices.size());
SOPHIA_ASSERT(this->vertexCount >= 3, "vertex count must be at least 3");
+11 -23
View File
@@ -1,8 +1,10 @@
#pragma once
#include <memory>
#include <sophia/aabb.hpp>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/mesh_data.hpp>
#include <sophia/types.hpp>
#include <vector>
@@ -13,47 +15,33 @@ namespace sophia {
class Mesh {
public:
struct Vertex {
glm::vec2 position{};
glm::vec3 color{};
// glm::vec3 normal{};
// glm::vec2 uv{};
// Binding 0 (per vertex, MeshVertex) for pipelines that draw meshes.
static std::vector<vk::VertexInputBindingDescription>
getBindingDescriptions();
static std::vector<vk::VertexInputAttributeDescription>
getAttributeDescriptions();
// bool operator==(const Vertex& other) const {
// return position == other.position && color == other.color;
// }
};
struct Builder {
std::vector<Vertex> vertices{};
std::vector<uint32_t> indices{};
// void loadModel(const std::string& filepath);
};
Mesh(Device& device, const Mesh::Builder& builder);
Mesh(Device& device, const MeshData& data);
~Mesh();
Mesh(const Mesh&) = delete;
Mesh& operator=(const Mesh&) = delete;
// static std::unique_ptr<Mesh> createMeshFromFile(
// Device& device, const std::string& filepath);
// Bounds of the vertices, in the mesh's own space.
const AABB& getBounds() const { return this->bounds; }
void bind(vk::CommandBuffer commandBuffer) const;
void draw(vk::CommandBuffer commandBuffer) const;
void draw(vk::CommandBuffer commandBuffer, u32 instanceCount = 1,
u32 firstInstance = 0) const;
private:
void createVertexBuffers(const std::vector<Vertex>& vertices);
void createVertexBuffers(const std::vector<MeshVertex>& vertices);
void createIndexBuffers(const std::vector<u32>& indices);
Device& device;
AABB bounds;
std::unique_ptr<Buffer> vertexBuffer;
u32 vertexCount;
@@ -1,12 +1,20 @@
#include "resources/pools/render_target_pool.hpp"
#include <tracy/Tracy.hpp>
namespace sophia {
RenderTargetPool::RenderTargetPool(Device& device, vk::Format format)
: device{device}, format{format} {}
: device{device},
format{format},
// Depth-only formats, so the images have no stencil aspect to track.
depthFormat{device.findSupportedFormat(
{vk::Format::eD32Sfloat, vk::Format::eX8D24UnormPack32},
vk::ImageTiling::eOptimal,
vk::FormatFeatureFlagBits::eDepthStencilAttachment)} {}
RenderTargetHandle RenderTargetPool::create(u32 width, u32 height) {
return pool.create(device, format, width, height);
return pool.create(device, format, depthFormat, width, height);
}
void RenderTargetPool::release(RenderTargetHandle handle) {
@@ -16,6 +24,7 @@ void RenderTargetPool::release(RenderTargetHandle handle) {
}
void RenderTargetPool::applyPendingResizes() {
ZoneScoped;
pool.forEach([](RenderTarget& target) { target.applyPendingResize(); });
}
@@ -24,11 +24,15 @@ class RenderTargetPool {
bool isAlive(RenderTargetHandle handle) const { return pool.isAlive(handle); }
vk::Format getFormat() const { return format; }
vk::Format getDepthFormat() const { return depthFormat; }
void applyPendingResizes();
private:
Device& device;
vk::Format format;
vk::Format depthFormat;
Pool<RenderTarget> pool{"RenderTarget"};
};
+55
View File
@@ -0,0 +1,55 @@
#include <array>
#include <sophia/primitives.hpp>
namespace sophia::primitives {
MeshData cube(float size, vec4 color) {
struct Face {
vec3 normal;
// Axes across the face, with u x v = normal, so corners taken
// counter-clockwise in (u, v) wind counter-clockwise seen from outside.
vec3 u;
vec3 v;
};
constexpr std::array<Face, 6> FACES{{
{{1.f, 0.f, 0.f}, {0.f, 0.f, -1.f}, {0.f, 1.f, 0.f}},
{{-1.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, {0.f, 1.f, 0.f}},
{{0.f, 1.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 0.f, -1.f}},
{{0.f, -1.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 0.f, 1.f}},
{{0.f, 0.f, 1.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}},
{{0.f, 0.f, -1.f}, {-1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}},
}};
constexpr std::array<vec2, 4> CORNERS{vec2{-1.f, -1.f}, vec2{1.f, -1.f},
vec2{1.f, 1.f}, vec2{-1.f, 1.f}};
float half = size / 2.f;
MeshData data;
data.vertices.reserve(FACES.size() * CORNERS.size());
data.indices.reserve(FACES.size() * 6);
for (const Face& face : FACES) {
auto base = static_cast<u32>(data.vertices.size());
for (vec2 corner : CORNERS) {
MeshVertex vertex;
vertex.position =
(face.normal + corner.x * face.u + corner.y * face.v) * half;
vertex.normal = face.normal;
// Texture v runs down while the face's v runs up.
vertex.uv = {(corner.x + 1.f) / 2.f, (1.f - corner.y) / 2.f};
vertex.color = color;
data.vertices.push_back(vertex);
}
for (u32 index : {0u, 1u, 2u, 2u, 3u, 0u}) {
data.indices.push_back(base + index);
}
}
return data;
}
} // namespace sophia::primitives
+14 -4
View File
@@ -4,9 +4,9 @@
namespace sophia {
RenderTarget::RenderTarget(Device& device, vk::Format format, u32 width,
u32 height)
: device{device}, format{format} {
RenderTarget::RenderTarget(Device& device, vk::Format format,
vk::Format depthFormat, u32 width, u32 height)
: device{device}, format{format}, depthFormat{depthFormat} {
create(std::max(width, 1u), std::max(height, 1u));
}
@@ -39,14 +39,24 @@ void RenderTarget::applyPendingResize() {
}
void RenderTarget::create(u32 width, u32 height) {
this->version++;
this->image = Image::Builder(this->device)
.setExtent(width, height)
.setFormat(this->format)
.asColorAttachment()
.withView()
.build();
this->depthImage = Image::Builder(this->device)
.setExtent(width, height)
.asDepthAttachment()
.setFormat(this->depthFormat)
.withView()
.build();
}
void RenderTarget::destroy() { this->image.reset(); }
void RenderTarget::destroy() {
this->image.reset();
this->depthImage.reset();
}
} // namespace sophia
+9 -3
View File
@@ -11,22 +11,25 @@
namespace sophia {
/**
* A place rendering can go. Currently always backed by an offscreen color
* image.
* A place rendering can go: an offscreen color image, plus a depth image of
* the same size.
*
* Owned by the ResourceRegistry and addressed through a RenderTargetHandle.
*/
class RenderTarget {
public:
RenderTarget(Device& device, vk::Format format, u32 width, u32 height);
RenderTarget(Device& device, vk::Format format, vk::Format depthFormat,
u32 width, u32 height);
~RenderTarget();
RenderTarget(const RenderTarget&) = delete;
RenderTarget& operator=(const RenderTarget&) = delete;
Image& getImage() const { return *this->image; }
Image& getDepthImage() const { return *this->depthImage; }
vk::Extent2D getExtent() const;
u32 getVersion() const { return this->version; }
void requestResize(u32 width, u32 height);
@@ -42,10 +45,13 @@ class RenderTarget {
Device& device;
vk::Format format;
vk::Format depthFormat;
std::unique_ptr<Image> image;
std::unique_ptr<Image> depthImage;
std::optional<vk::Extent2D> pendingExtent;
u32 version = 0;
};
} // namespace sophia
+11 -2
View File
@@ -1,10 +1,12 @@
#pragma once
#include <array>
#include <sophia/handles.hpp>
#include <sophia/util/pool.hpp>
#include "resources/mesh.hpp"
#include "resources/pools/render_target_pool.hpp"
#include "resources/texture.hpp"
#include "vulkan/device.hpp"
namespace sophia {
@@ -15,13 +17,20 @@ class ResourceRegistry {
ResourceRegistry& operator=(const ResourceRegistry&) = delete;
ResourceRegistry(Device& device, vk::Format renderTargetFormat)
: device{device}, renderTargetPool{device, renderTargetFormat} {}
: device{device}, renderTargetPool{device, renderTargetFormat} {
constexpr std::array<u8, 4> WHITE{0xFF, 0xFF, 0xFF, 0xFF};
whiteTexture = texturePool.create(device, 1, 1, WHITE);
}
Device& device;
Pool<Mesh> meshPool{"Mesh"};
RenderTargetPool renderTargetPool;
// TexturePool textures;
Pool<Texture> texturePool{"Texture"};
// MaterialPool materials;
// 1x1 white. Stands in for sprites without a texture, so they draw as a
// solid quad in their tint color.
TextureHandle whiteTexture = INVALID_HANDLE;
};
} // namespace sophia
+62 -10
View File
@@ -1,5 +1,9 @@
#include <stb_image.h>
#include <sophia/resource_registry_ref.hpp>
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include "resources/resource_registry.hpp"
@@ -10,12 +14,34 @@ MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path) {
(void)path;
SOPHIA_ASSERT(false,
"Mesh::load(path) is not implemented yet - no model file "
"format is supported; build one via Mesh::Builder for now");
"format is supported; build a MeshData instead");
return MeshHandle{};
}
template <>
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path) {
int width = 0;
int height = 0;
int channels = 0;
// Always decode to RGBA8, whatever the file has.
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, 4);
if (pixels == nullptr) {
log::fatal("failed to load texture {} - {}", path, stbi_failure_reason());
throw std::runtime_error("failed to load texture: " + path);
}
auto size = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
TextureHandle handle = registry->texturePool.create(
registry->device, static_cast<u32>(width), static_cast<u32>(height),
std::span<const u8>{pixels, size});
stbi_image_free(pixels);
return handle;
}
template <>
void ResourceRegistryRef::release<Mesh>(MeshHandle handle) {
// Frames in flight may still be drawing it.
registry->device.waitIdle();
registry->meshPool.release(handle);
}
@@ -24,17 +50,18 @@ void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle) {
registry->renderTargetPool.release(handle);
}
MeshHandle ResourceRegistryRef::createResource(
std::type_identity<Mesh>, std::vector<CanvasVertex> vertices,
std::vector<u32> indices) {
Mesh::Builder builder{};
builder.vertices.reserve(vertices.size());
for (const CanvasVertex& vertex : vertices) {
builder.vertices.push_back({vertex.position, vertex.color});
template <>
void ResourceRegistryRef::release<Texture>(TextureHandle handle) {
SOPHIA_ASSERT(handle != registry->whiteTexture,
"the built-in white texture cannot be released");
// Frames in flight may still be sampling it.
registry->device.waitIdle();
registry->texturePool.release(handle);
}
builder.indices = std::move(indices);
return registry->meshPool.create(registry->device, builder);
MeshHandle ResourceRegistryRef::createResource(std::type_identity<Mesh>,
const MeshData& data) {
return registry->meshPool.create(registry->device, data);
}
RenderTargetHandle ResourceRegistryRef::createResource(
@@ -42,6 +69,13 @@ RenderTargetHandle ResourceRegistryRef::createResource(
return registry->renderTargetPool.create(width, height);
}
TextureHandle ResourceRegistryRef::createResource(std::type_identity<Texture>,
u32 width, u32 height,
std::vector<u8> pixels) {
return registry->texturePool.create(registry->device, width, height,
std::span<const u8>{pixels});
}
void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width,
u32 height) {
registry->renderTargetPool.get(handle).requestResize(width, height);
@@ -52,8 +86,26 @@ glm::vec2 ResourceRegistryRef::getExtent(RenderTargetHandle handle) const {
return {static_cast<float>(extent.width), static_cast<float>(extent.height)};
}
AABB ResourceRegistryRef::getBounds(MeshHandle handle) const {
return registry->meshPool.get(handle).getBounds();
}
bool ResourceRegistryRef::isAlive(MeshHandle handle) const {
return registry->meshPool.isAlive(handle);
}
bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const {
return registry->renderTargetPool.isAlive(handle);
}
glm::vec2 ResourceRegistryRef::getExtent(TextureHandle handle) const {
const Texture& texture = registry->texturePool.get(handle);
return {static_cast<float>(texture.getWidth()),
static_cast<float>(texture.getHeight())};
}
bool ResourceRegistryRef::isAlive(TextureHandle handle) const {
return registry->texturePool.isAlive(handle);
}
} // namespace sophia
+32
View File
@@ -0,0 +1,32 @@
#include "resources/texture.hpp"
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
namespace sophia {
Texture::Texture(Device& device, u32 width, u32 height,
std::span<const u8> pixels) {
SOPHIA_ASSERT(width > 0 && height > 0, "texture must not be empty");
SOPHIA_ASSERT(pixels.size() == static_cast<size_t>(width) * height * 4,
"texture pixel data must be width * height RGBA8 texels");
this->image = Image::Builder(device)
.setExtent(width, height)
.setFormat(vk::Format::eR8G8B8A8Srgb)
.setUsage(vk::ImageUsageFlagBits::eSampled)
.setInitialData(pixels.data(), pixels.size())
.withView()
.withSampler(vk::Filter::eLinear,
vk::SamplerAddressMode::eClampToEdge)
.build();
}
Texture::~Texture() { log::trace("destroyed sophia::Texture"); }
vk::DescriptorImageInfo Texture::descriptorInfo() const {
return {this->image->getSampler(), this->image->getView(),
vk::ImageLayout::eShaderReadOnlyOptimal};
}
} // namespace sophia
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <memory>
#include <sophia/types.hpp>
#include <span>
#include <vulkan/vulkan.hpp>
#include "vulkan/device.hpp"
#include "vulkan/image.hpp"
namespace sophia {
/**
* A sampled, read-only RGBA8 (sRGB) image.
*
* Owned by the ResourceRegistry and addressed through a TextureHandle.
*/
class Texture {
public:
/** `pixels` is width * height tightly packed RGBA8 texels, top row first. */
Texture(Device& device, u32 width, u32 height, std::span<const u8> pixels);
~Texture();
Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete;
const Image& getImage() const { return *this->image; }
u32 getWidth() const { return this->image->getExtent().width; }
u32 getHeight() const { return this->image->getExtent().height; }
vk::DescriptorImageInfo descriptorInfo() const;
private:
std::unique_ptr<Image> image;
};
} // namespace sophia
+34 -6
View File
@@ -1,16 +1,44 @@
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/scene.hpp>
#include <sophia/util/logger.hpp>
namespace sophia {
Scene::Scene() {
entt::entity entity = this->registry.create();
auto& a = this->registry.emplace<TransformComponent>(entity);
auto& b = this->registry.get<TransformComponent>(entity);
}
Scene::Scene() {}
Scene::~Scene() { log::trace("destroying sophia::Scene"); }
Entity Scene::createEntity(const std::string& name) {
Entity entity{this->registry.create(), this};
entity.addComponent<TagComponent>(name.empty() ? "Entity" : name);
entity.addComponent<TransformComponent>();
return entity;
}
Entity Scene::createCamera(const std::string& name) {
Entity camera = createEntity(name);
camera.addComponent<CameraComponent>();
if (this->activeCamera == entt::null) {
this->activeCamera = camera;
}
return camera;
}
Entity Scene::getActiveCamera() {
if (this->activeCamera == entt::null) {
return {};
}
return {this->activeCamera, this};
}
void Scene::setActiveCamera(Entity camera) { this->activeCamera = camera; }
void Scene::destroyEntity(Entity entity) {
if (this->activeCamera == entity) {
this->activeCamera = entt::null;
}
this->registry.destroy(entity);
}
} // namespace sophia
@@ -0,0 +1,9 @@
#include "scene/scene_render_packet_builder.hpp"
namespace sophia {
void SceneRenderPacketBuilder::build(SceneHandle sceneHandle,
RenderTargetHandle renderTargetHandle,
RenderPacket& renderPacket) {}
} // namespace sophia
@@ -0,0 +1,16 @@
#pragma once
#include <sophia/handles.hpp>
#include "renderer/renderer.hpp"
namespace sophia {
class SceneRenderPacketBuilder {
public:
static void build(SceneHandle sceneHandle,
RenderTargetHandle renderTargetHandle,
RenderPacket& renderPacket);
};
} // namespace sophia
+60 -89
View File
@@ -6,95 +6,66 @@
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 355> fullscreen_vert_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000032,
0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0008000f, 0x00000000, 0x00000004, 0x6e69616d,
0x00000000, 0x00000018, 0x00000022, 0x0000002c,
0x00030003, 0x00000002, 0x000001c2, 0x000a0004,
0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70,
0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365,
0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f,
0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572,
0x00657669, 0x00040005, 0x00000004, 0x6e69616d,
0x00000000, 0x00050005, 0x0000000c, 0x69736f70,
0x6e6f6974, 0x00000073, 0x00030005, 0x00000015,
0x00736f70, 0x00060005, 0x00000018, 0x565f6c67,
0x65747265, 0x646e4978, 0x00007865, 0x00060005,
0x00000020, 0x505f6c67, 0x65567265, 0x78657472,
0x00000000, 0x00060006, 0x00000020, 0x00000000,
0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006,
0x00000020, 0x00000001, 0x505f6c67, 0x746e696f,
0x657a6953, 0x00000000, 0x00070006, 0x00000020,
0x00000002, 0x435f6c67, 0x4470696c, 0x61747369,
0x0065636e, 0x00070006, 0x00000020, 0x00000003,
0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e,
0x00030005, 0x00000022, 0x00000000, 0x00030005,
0x0000002c, 0x00007675, 0x00040047, 0x00000018,
0x0000000b, 0x0000002a, 0x00030047, 0x00000020,
0x00000002, 0x00050048, 0x00000020, 0x00000000,
0x0000000b, 0x00000000, 0x00050048, 0x00000020,
0x00000001, 0x0000000b, 0x00000001, 0x00050048,
0x00000020, 0x00000002, 0x0000000b, 0x00000003,
0x00050048, 0x00000020, 0x00000003, 0x0000000b,
0x00000004, 0x00040047, 0x0000002c, 0x0000001e,
0x00000000, 0x00020013, 0x00000002, 0x00030021,
0x00000003, 0x00000002, 0x00030016, 0x00000006,
0x00000020, 0x00040017, 0x00000007, 0x00000006,
0x00000002, 0x00040015, 0x00000008, 0x00000020,
0x00000000, 0x0004002b, 0x00000008, 0x00000009,
0x00000006, 0x0004001c, 0x0000000a, 0x00000007,
0x00000009, 0x00040020, 0x0000000b, 0x00000006,
0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c,
0x00000006, 0x0004002b, 0x00000006, 0x0000000d,
0xbf800000, 0x0005002c, 0x00000007, 0x0000000e,
0x0000000d, 0x0000000d, 0x0004002b, 0x00000006,
0x0000000f, 0x3f800000, 0x0005002c, 0x00000007,
0x00000010, 0x0000000f, 0x0000000d, 0x0005002c,
0x00000007, 0x00000011, 0x0000000d, 0x0000000f,
0x0005002c, 0x00000007, 0x00000012, 0x0000000f,
0x0000000f, 0x0009002c, 0x0000000a, 0x00000013,
0x0000000e, 0x00000010, 0x00000011, 0x00000011,
0x00000010, 0x00000012, 0x00040020, 0x00000014,
0x00000007, 0x00000007, 0x00040015, 0x00000016,
0x00000020, 0x00000001, 0x00040020, 0x00000017,
0x00000001, 0x00000016, 0x0004003b, 0x00000017,
0x00000018, 0x00000001, 0x00040020, 0x0000001a,
0x00000006, 0x00000007, 0x00040017, 0x0000001d,
0x00000006, 0x00000004, 0x0004002b, 0x00000008,
0x0000001e, 0x00000001, 0x0004001c, 0x0000001f,
0x00000006, 0x0000001e, 0x0006001e, 0x00000020,
0x0000001d, 0x00000006, 0x0000001f, 0x0000001f,
0x00040020, 0x00000021, 0x00000003, 0x00000020,
0x0004003b, 0x00000021, 0x00000022, 0x00000003,
0x0004002b, 0x00000016, 0x00000023, 0x00000000,
0x0004002b, 0x00000006, 0x00000025, 0x00000000,
0x00040020, 0x00000029, 0x00000003, 0x0000001d,
0x00040020, 0x0000002b, 0x00000003, 0x00000007,
0x0004003b, 0x0000002b, 0x0000002c, 0x00000003,
0x0004002b, 0x00000006, 0x0000002e, 0x3f000000,
0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x0004003b,
0x00000014, 0x00000015, 0x00000007, 0x0003003e,
0x0000000c, 0x00000013, 0x0004003d, 0x00000016,
0x00000019, 0x00000018, 0x00050041, 0x0000001a,
0x0000001b, 0x0000000c, 0x00000019, 0x0004003d,
0x00000007, 0x0000001c, 0x0000001b, 0x0003003e,
0x00000015, 0x0000001c, 0x0004003d, 0x00000007,
0x00000024, 0x00000015, 0x00050051, 0x00000006,
0x00000026, 0x00000024, 0x00000000, 0x00050051,
0x00000006, 0x00000027, 0x00000024, 0x00000001,
0x00070050, 0x0000001d, 0x00000028, 0x00000026,
0x00000027, 0x00000025, 0x0000000f, 0x00050041,
0x00000029, 0x0000002a, 0x00000022, 0x00000023,
0x0003003e, 0x0000002a, 0x00000028, 0x0004003d,
0x00000007, 0x0000002d, 0x00000015, 0x0005008e,
0x00000007, 0x0000002f, 0x0000002d, 0x0000002e,
0x00050050, 0x00000007, 0x00000030, 0x0000002e,
0x0000002e, 0x00050081, 0x00000007, 0x00000031,
0x0000002f, 0x00000030, 0x0003003e, 0x0000002c,
0x00000031, 0x000100fd, 0x00010038,
0x07230203, 0x00010000, 0x000d000b, 0x00000032, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000000,
0x00000004, 0x6e69616d, 0x00000000, 0x00000018, 0x00000022, 0x0000002c,
0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f,
0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365,
0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63,
0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004, 0x6e69616d,
0x00000000, 0x00050005, 0x0000000c, 0x69736f70, 0x6e6f6974, 0x00000073,
0x00030005, 0x00000015, 0x00736f70, 0x00060005, 0x00000018, 0x565f6c67,
0x65747265, 0x646e4978, 0x00007865, 0x00060005, 0x00000020, 0x505f6c67,
0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x00000020, 0x00000000,
0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, 0x00000020, 0x00000001,
0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x00000020,
0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006,
0x00000020, 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e,
0x00030005, 0x00000022, 0x00000000, 0x00030005, 0x0000002c, 0x00007675,
0x00040047, 0x00000018, 0x0000000b, 0x0000002a, 0x00030047, 0x00000020,
0x00000002, 0x00050048, 0x00000020, 0x00000000, 0x0000000b, 0x00000000,
0x00050048, 0x00000020, 0x00000001, 0x0000000b, 0x00000001, 0x00050048,
0x00000020, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x00000020,
0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x0000002c, 0x0000001e,
0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006,
0x00000002, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b,
0x00000008, 0x00000009, 0x00000006, 0x0004001c, 0x0000000a, 0x00000007,
0x00000009, 0x00040020, 0x0000000b, 0x00000006, 0x0000000a, 0x0004003b,
0x0000000b, 0x0000000c, 0x00000006, 0x0004002b, 0x00000006, 0x0000000d,
0xbf800000, 0x0005002c, 0x00000007, 0x0000000e, 0x0000000d, 0x0000000d,
0x0004002b, 0x00000006, 0x0000000f, 0x3f800000, 0x0005002c, 0x00000007,
0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, 0x00000007, 0x00000011,
0x0000000d, 0x0000000f, 0x0005002c, 0x00000007, 0x00000012, 0x0000000f,
0x0000000f, 0x0009002c, 0x0000000a, 0x00000013, 0x0000000e, 0x00000010,
0x00000011, 0x00000011, 0x00000010, 0x00000012, 0x00040020, 0x00000014,
0x00000007, 0x00000007, 0x00040015, 0x00000016, 0x00000020, 0x00000001,
0x00040020, 0x00000017, 0x00000001, 0x00000016, 0x0004003b, 0x00000017,
0x00000018, 0x00000001, 0x00040020, 0x0000001a, 0x00000006, 0x00000007,
0x00040017, 0x0000001d, 0x00000006, 0x00000004, 0x0004002b, 0x00000008,
0x0000001e, 0x00000001, 0x0004001c, 0x0000001f, 0x00000006, 0x0000001e,
0x0006001e, 0x00000020, 0x0000001d, 0x00000006, 0x0000001f, 0x0000001f,
0x00040020, 0x00000021, 0x00000003, 0x00000020, 0x0004003b, 0x00000021,
0x00000022, 0x00000003, 0x0004002b, 0x00000016, 0x00000023, 0x00000000,
0x0004002b, 0x00000006, 0x00000025, 0x00000000, 0x00040020, 0x00000029,
0x00000003, 0x0000001d, 0x00040020, 0x0000002b, 0x00000003, 0x00000007,
0x0004003b, 0x0000002b, 0x0000002c, 0x00000003, 0x0004002b, 0x00000006,
0x0000002e, 0x3f000000, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000014, 0x00000015,
0x00000007, 0x0003003e, 0x0000000c, 0x00000013, 0x0004003d, 0x00000016,
0x00000019, 0x00000018, 0x00050041, 0x0000001a, 0x0000001b, 0x0000000c,
0x00000019, 0x0004003d, 0x00000007, 0x0000001c, 0x0000001b, 0x0003003e,
0x00000015, 0x0000001c, 0x0004003d, 0x00000007, 0x00000024, 0x00000015,
0x00050051, 0x00000006, 0x00000026, 0x00000024, 0x00000000, 0x00050051,
0x00000006, 0x00000027, 0x00000024, 0x00000001, 0x00070050, 0x0000001d,
0x00000028, 0x00000026, 0x00000027, 0x00000025, 0x0000000f, 0x00050041,
0x00000029, 0x0000002a, 0x00000022, 0x00000023, 0x0003003e, 0x0000002a,
0x00000028, 0x0004003d, 0x00000007, 0x0000002d, 0x00000015, 0x0005008e,
0x00000007, 0x0000002f, 0x0000002d, 0x0000002e, 0x00050050, 0x00000007,
0x00000030, 0x0000002e, 0x0000002e, 0x00050081, 0x00000007, 0x00000031,
0x0000002f, 0x00000030, 0x0003003e, 0x0000002c, 0x00000031, 0x000100fd,
0x00010038,
};
} // namespace sophia::shaders
+9
View File
@@ -0,0 +1,9 @@
#version 450
layout (location = 0) in vec4 fragColor;
layout (location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor.rgb, 1.0);
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 151> mesh_frag_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000014, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004,
0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000b, 0x00030010,
0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004,
0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c,
0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f,
0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005,
0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, 0x4374756f,
0x726f6c6f, 0x00000000, 0x00050005, 0x0000000b, 0x67617266, 0x6f6c6f43,
0x00000072, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047,
0x0000000b, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021,
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017,
0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003,
0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040020,
0x0000000a, 0x00000001, 0x00000007, 0x0004003b, 0x0000000a, 0x0000000b,
0x00000001, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 0x0004002b,
0x00000006, 0x0000000f, 0x3f800000, 0x00050036, 0x00000002, 0x00000004,
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x00000007,
0x0000000d, 0x0000000b, 0x0008004f, 0x0000000c, 0x0000000e, 0x0000000d,
0x0000000d, 0x00000000, 0x00000001, 0x00000002, 0x00050051, 0x00000006,
0x00000010, 0x0000000e, 0x00000000, 0x00050051, 0x00000006, 0x00000011,
0x0000000e, 0x00000001, 0x00050051, 0x00000006, 0x00000012, 0x0000000e,
0x00000002, 0x00070050, 0x00000007, 0x00000013, 0x00000010, 0x00000011,
0x00000012, 0x0000000f, 0x0003003e, 0x00000009, 0x00000013, 0x000100fd,
0x00010038,
};
} // namespace sophia::shaders
+23
View File
@@ -0,0 +1,23 @@
#version 450
// Per vertex; layout must match MeshVertex. Normal and UV are unused until
// there is lighting and texturing.
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 uv;
layout (location = 3) in vec4 color;
// Per instance; layout must match MeshRenderSystem::Instance.
layout (location = 4) in mat4 model;
layout (location = 8) in vec4 instanceColor;
layout (location = 0) out vec4 fragColor;
layout (push_constant) uniform Push {
mat4 viewProjection;
} push;
void main() {
gl_Position = push.viewProjection * model * vec4(position, 1.0);
fragColor = color * instanceColor;
}
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 411> mesh_vert_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000032, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000d000f, 0x00000000,
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000018, 0x0000001d,
0x00000027, 0x00000029, 0x0000002b, 0x0000002e, 0x00000031, 0x00030003,
0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45,
0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576,
0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65,
0x74636572, 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000,
0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, 0x78657472, 0x00000000,
0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69,
0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953,
0x00000000, 0x00070006, 0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c,
0x61747369, 0x0065636e, 0x00070006, 0x0000000b, 0x00000003, 0x435f6c67,
0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000,
0x00040005, 0x00000011, 0x68737550, 0x00000000, 0x00070006, 0x00000011,
0x00000000, 0x77656976, 0x6a6f7250, 0x69746365, 0x00006e6f, 0x00040005,
0x00000013, 0x68737570, 0x00000000, 0x00040005, 0x00000018, 0x65646f6d,
0x0000006c, 0x00050005, 0x0000001d, 0x69736f70, 0x6e6f6974, 0x00000000,
0x00050005, 0x00000027, 0x67617266, 0x6f6c6f43, 0x00000072, 0x00040005,
0x00000029, 0x6f6c6f63, 0x00000072, 0x00060005, 0x0000002b, 0x74736e69,
0x65636e61, 0x6f6c6f43, 0x00000072, 0x00040005, 0x0000002e, 0x6d726f6e,
0x00006c61, 0x00030005, 0x00000031, 0x00007675, 0x00030047, 0x0000000b,
0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000,
0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048,
0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x0000000b,
0x00000003, 0x0000000b, 0x00000004, 0x00030047, 0x00000011, 0x00000002,
0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, 0x00000011,
0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000,
0x00000023, 0x00000000, 0x00040047, 0x00000018, 0x0000001e, 0x00000004,
0x00040047, 0x0000001d, 0x0000001e, 0x00000000, 0x00040047, 0x00000027,
0x0000001e, 0x00000000, 0x00040047, 0x00000029, 0x0000001e, 0x00000003,
0x00040047, 0x0000002b, 0x0000001e, 0x00000008, 0x00040047, 0x0000002e,
0x0000001e, 0x00000001, 0x00040047, 0x00000031, 0x0000001e, 0x00000002,
0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004,
0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008,
0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009,
0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a,
0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c,
0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001,
0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040018, 0x00000010,
0x00000007, 0x00000004, 0x0003001e, 0x00000011, 0x00000010, 0x00040020,
0x00000012, 0x00000009, 0x00000011, 0x0004003b, 0x00000012, 0x00000013,
0x00000009, 0x00040020, 0x00000014, 0x00000009, 0x00000010, 0x00040020,
0x00000017, 0x00000001, 0x00000010, 0x0004003b, 0x00000017, 0x00000018,
0x00000001, 0x00040017, 0x0000001b, 0x00000006, 0x00000003, 0x00040020,
0x0000001c, 0x00000001, 0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d,
0x00000001, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x00040020,
0x00000025, 0x00000003, 0x00000007, 0x0004003b, 0x00000025, 0x00000027,
0x00000003, 0x00040020, 0x00000028, 0x00000001, 0x00000007, 0x0004003b,
0x00000028, 0x00000029, 0x00000001, 0x0004003b, 0x00000028, 0x0000002b,
0x00000001, 0x0004003b, 0x0000001c, 0x0000002e, 0x00000001, 0x00040017,
0x0000002f, 0x00000006, 0x00000002, 0x00040020, 0x00000030, 0x00000001,
0x0000002f, 0x0004003b, 0x00000030, 0x00000031, 0x00000001, 0x00050036,
0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005,
0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d,
0x00000010, 0x00000016, 0x00000015, 0x0004003d, 0x00000010, 0x00000019,
0x00000018, 0x00050092, 0x00000010, 0x0000001a, 0x00000016, 0x00000019,
0x0004003d, 0x0000001b, 0x0000001e, 0x0000001d, 0x00050051, 0x00000006,
0x00000020, 0x0000001e, 0x00000000, 0x00050051, 0x00000006, 0x00000021,
0x0000001e, 0x00000001, 0x00050051, 0x00000006, 0x00000022, 0x0000001e,
0x00000002, 0x00070050, 0x00000007, 0x00000023, 0x00000020, 0x00000021,
0x00000022, 0x0000001f, 0x00050091, 0x00000007, 0x00000024, 0x0000001a,
0x00000023, 0x00050041, 0x00000025, 0x00000026, 0x0000000d, 0x0000000f,
0x0003003e, 0x00000026, 0x00000024, 0x0004003d, 0x00000007, 0x0000002a,
0x00000029, 0x0004003d, 0x00000007, 0x0000002c, 0x0000002b, 0x00050085,
0x00000007, 0x0000002d, 0x0000002a, 0x0000002c, 0x0003003e, 0x00000027,
0x0000002d, 0x000100fd, 0x00010038,
};
} // namespace sophia::shaders
+2 -2
View File
@@ -1,9 +1,9 @@
#version 450
layout (location = 0) in vec3 fragColor;
layout (location = 0) in vec4 fragColor;
layout (location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
outColor = fragColor;
}
@@ -5,31 +5,36 @@
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 143> model_render_system_frag_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000013, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004,
0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010,
0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004,
0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c,
0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f,
0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005,
0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, 0x4374756f,
0x726f6c6f, 0x00000000, 0x00050005, 0x0000000c, 0x67617266, 0x6f6c6f43,
0x00000072, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047,
0x0000000c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021,
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017,
0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003,
0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040017,
0x0000000a, 0x00000006, 0x00000003, 0x00040020, 0x0000000b, 0x00000001,
0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000001, 0x0004002b,
0x00000006, 0x0000000e, 0x3f800000, 0x00050036, 0x00000002, 0x00000004,
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000a,
0x0000000d, 0x0000000c, 0x00050051, 0x00000006, 0x0000000f, 0x0000000d,
0x00000000, 0x00050051, 0x00000006, 0x00000010, 0x0000000d, 0x00000001,
0x00050051, 0x00000006, 0x00000011, 0x0000000d, 0x00000002, 0x00070050,
0x00000007, 0x00000012, 0x0000000f, 0x00000010, 0x00000011, 0x0000000e,
0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038,
inline constexpr std::array<uint32_t, 113> model_render_system_frag_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x0000000d,
0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d,
0x00000000, 0x00000009, 0x0000000b, 0x00030010,
0x00000004, 0x00000007, 0x00030003, 0x00000002,
0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f,
0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c,
0x7269645f, 0x69746365, 0x00006576, 0x00080004,
0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63,
0x69645f65, 0x74636572, 0x00657669, 0x00040005,
0x00000004, 0x6e69616d, 0x00000000, 0x00050005,
0x00000009, 0x4374756f, 0x726f6c6f, 0x00000000,
0x00050005, 0x0000000b, 0x67617266, 0x6f6c6f43,
0x00000072, 0x00040047, 0x00000009, 0x0000001e,
0x00000000, 0x00040047, 0x0000000b, 0x0000001e,
0x00000000, 0x00020013, 0x00000002, 0x00030021,
0x00000003, 0x00000002, 0x00030016, 0x00000006,
0x00000020, 0x00040017, 0x00000007, 0x00000006,
0x00000004, 0x00040020, 0x00000008, 0x00000003,
0x00000007, 0x0004003b, 0x00000008, 0x00000009,
0x00000003, 0x00040020, 0x0000000a, 0x00000001,
0x00000007, 0x0004003b, 0x0000000a, 0x0000000b,
0x00000001, 0x00050036, 0x00000002, 0x00000004,
0x00000000, 0x00000003, 0x000200f8, 0x00000005,
0x0004003d, 0x00000007, 0x0000000c, 0x0000000b,
0x0003003e, 0x00000009, 0x0000000c, 0x000100fd,
0x00010038,
};
} // namespace sophia::shaders
+5 -4
View File
@@ -1,9 +1,10 @@
#version 450
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;
// Per vertex; locations match Mesh::getAttributeDescriptions().
layout (location = 0) in vec3 position;
layout (location = 3) in vec4 color;
layout (location = 0) out vec3 fragColor;
layout (location = 0) out vec4 fragColor;
layout (push_constant) uniform Push {
mat4 transform;
@@ -11,6 +12,6 @@ layout (push_constant) uniform Push {
} push;
void main() {
gl_Position = vec4(push.transform * vec4(position, 0.0, 1.0));
gl_Position = push.transform * vec4(position, 1.0);
fragColor = color;
}
@@ -5,12 +5,12 @@
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 345> model_render_system_vert_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000029, 0x00000000, 0x00020011,
inline constexpr std::array<uint32_t, 338> model_render_system_vert_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000027, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0009000f, 0x00000000,
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000025,
0x00000027, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47,
0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000023,
0x00000025, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47,
0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f,
0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45,
0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004,
@@ -24,8 +24,8 @@ inline constexpr std::array<uint32_t, 345> model_render_system_vert_spv = {
0x00060006, 0x00000011, 0x00000000, 0x6e617274, 0x726f6673, 0x0000006d,
0x00050006, 0x00000011, 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005,
0x00000013, 0x68737570, 0x00000000, 0x00050005, 0x00000019, 0x69736f70,
0x6e6f6974, 0x00000000, 0x00050005, 0x00000025, 0x67617266, 0x6f6c6f43,
0x00000072, 0x00040005, 0x00000027, 0x6f6c6f63, 0x00000072, 0x00030047,
0x6e6f6974, 0x00000000, 0x00050005, 0x00000023, 0x67617266, 0x6f6c6f43,
0x00000072, 0x00040005, 0x00000025, 0x6f6c6f63, 0x00000072, 0x00030047,
0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b,
0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001,
0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048,
@@ -34,8 +34,8 @@ inline constexpr std::array<uint32_t, 345> model_render_system_vert_spv = {
0x00000011, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011,
0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000011, 0x00000001,
0x00000023, 0x00000040, 0x00040047, 0x00000019, 0x0000001e, 0x00000000,
0x00040047, 0x00000025, 0x0000001e, 0x00000000, 0x00040047, 0x00000027,
0x0000001e, 0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
0x00040047, 0x00000023, 0x0000001e, 0x00000000, 0x00040047, 0x00000025,
0x0000001e, 0x00000003, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000,
0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a,
@@ -46,24 +46,23 @@ inline constexpr std::array<uint32_t, 345> model_render_system_vert_spv = {
0x00040018, 0x00000010, 0x00000007, 0x00000004, 0x0004001e, 0x00000011,
0x00000010, 0x00000007, 0x00040020, 0x00000012, 0x00000009, 0x00000011,
0x0004003b, 0x00000012, 0x00000013, 0x00000009, 0x00040020, 0x00000014,
0x00000009, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000002,
0x00000009, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000003,
0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018,
0x00000019, 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x00000000,
0x0004002b, 0x00000006, 0x0000001c, 0x3f800000, 0x00040020, 0x00000021,
0x00000003, 0x00000007, 0x00040017, 0x00000023, 0x00000006, 0x00000003,
0x00040020, 0x00000024, 0x00000003, 0x00000023, 0x0004003b, 0x00000024,
0x00000025, 0x00000003, 0x00040020, 0x00000026, 0x00000001, 0x00000023,
0x0004003b, 0x00000026, 0x00000027, 0x00000001, 0x00050036, 0x00000002,
0x00000019, 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x3f800000,
0x00040020, 0x00000021, 0x00000003, 0x00000007, 0x0004003b, 0x00000021,
0x00000023, 0x00000003, 0x00040020, 0x00000024, 0x00000001, 0x00000007,
0x0004003b, 0x00000024, 0x00000025, 0x00000001, 0x00050036, 0x00000002,
0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041,
0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, 0x00000010,
0x00000016, 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019,
0x00050051, 0x00000006, 0x0000001d, 0x0000001a, 0x00000000, 0x00050051,
0x00000006, 0x0000001e, 0x0000001a, 0x00000001, 0x00070050, 0x00000007,
0x0000001f, 0x0000001d, 0x0000001e, 0x0000001b, 0x0000001c, 0x00050091,
0x00000007, 0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021,
0x00000022, 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020,
0x0004003d, 0x00000023, 0x00000028, 0x00000027, 0x0003003e, 0x00000025,
0x00000028, 0x000100fd, 0x00010038,
0x00050051, 0x00000006, 0x0000001c, 0x0000001a, 0x00000000, 0x00050051,
0x00000006, 0x0000001d, 0x0000001a, 0x00000001, 0x00050051, 0x00000006,
0x0000001e, 0x0000001a, 0x00000002, 0x00070050, 0x00000007, 0x0000001f,
0x0000001c, 0x0000001d, 0x0000001e, 0x0000001b, 0x00050091, 0x00000007,
0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, 0x00000022,
0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, 0x0004003d,
0x00000007, 0x00000026, 0x00000025, 0x0003003e, 0x00000023, 0x00000026,
0x000100fd, 0x00010038,
};
} // namespace sophia::shaders
+12
View File
@@ -0,0 +1,12 @@
#version 450
layout (location = 0) in vec4 fragColor;
layout (location = 1) in vec2 fragUv;
layout (location = 0) out vec4 outColor;
layout (set = 0, binding = 0) uniform sampler2D spriteTexture;
void main() {
outColor = texture(spriteTexture, fragUv) * fragColor;
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 186> sprite_frag_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000018, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000004,
0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x00000011, 0x00000015,
0x00030010, 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2,
0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79,
0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47,
0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669,
0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009,
0x4374756f, 0x726f6c6f, 0x00000000, 0x00060005, 0x0000000d, 0x69727073,
0x65546574, 0x72757478, 0x00000065, 0x00040005, 0x00000011, 0x67617266,
0x00007655, 0x00050005, 0x00000015, 0x67617266, 0x6f6c6f43, 0x00000072,
0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000d,
0x00000021, 0x00000000, 0x00040047, 0x0000000d, 0x00000022, 0x00000000,
0x00040047, 0x00000011, 0x0000001e, 0x00000001, 0x00040047, 0x00000015,
0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007,
0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00090019, 0x0000000a,
0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c,
0x00000000, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000000,
0x00040017, 0x0000000f, 0x00000006, 0x00000002, 0x00040020, 0x00000010,
0x00000001, 0x0000000f, 0x0004003b, 0x00000010, 0x00000011, 0x00000001,
0x00040020, 0x00000014, 0x00000001, 0x00000007, 0x0004003b, 0x00000014,
0x00000015, 0x00000001, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000b, 0x0000000e,
0x0000000d, 0x0004003d, 0x0000000f, 0x00000012, 0x00000011, 0x00050057,
0x00000007, 0x00000013, 0x0000000e, 0x00000012, 0x0004003d, 0x00000007,
0x00000016, 0x00000015, 0x00050085, 0x00000007, 0x00000017, 0x00000013,
0x00000016, 0x0003003e, 0x00000009, 0x00000017, 0x000100fd, 0x00010038,
};
} // namespace sophia::shaders
+29
View File
@@ -0,0 +1,29 @@
#version 450
// Per instance; layout must match SpriteRenderSystem::Instance.
layout (location = 0) in mat4 model; // Maps the unit quad to world space.
layout (location = 4) in vec4 color;
layout (location = 5) in vec4 uvRect;
layout (location = 0) out vec4 fragColor;
layout (location = 1) out vec2 fragUv;
layout (push_constant) uniform Push {
mat4 viewProjection;
} push;
// Two triangles covering the unit quad (0,0)-(1,1).
const vec2 CORNERS[6] = vec2[](
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0),
vec2(1.0, 1.0), vec2(0.0, 1.0), vec2(0.0, 0.0)
);
void main() {
vec2 corner = CORNERS[gl_VertexIndex];
gl_Position = push.viewProjection * model * vec4(corner, 0.0, 1.0);
fragColor = color;
// The quad's y runs up and the texture's v runs down.
fragUv = mix(uvRect.xy, uvRect.zw, vec2(corner.x, 1.0 - corner.y));
}
+100
View File
@@ -0,0 +1,100 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 534> sprite_vert_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000049, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000c000f, 0x00000000,
0x00000004, 0x6e69616d, 0x00000000, 0x00000016, 0x00000021, 0x0000002b,
0x00000035, 0x00000037, 0x0000003a, 0x0000003b, 0x00030003, 0x00000002,
0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70,
0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004,
0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572,
0x00657669, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00040005,
0x00000009, 0x6e726f63, 0x00007265, 0x00060005, 0x00000016, 0x565f6c67,
0x65747265, 0x646e4978, 0x00007865, 0x00050005, 0x00000019, 0x65646e69,
0x6c626178, 0x00000065, 0x00060005, 0x0000001f, 0x505f6c67, 0x65567265,
0x78657472, 0x00000000, 0x00060006, 0x0000001f, 0x00000000, 0x505f6c67,
0x7469736f, 0x006e6f69, 0x00070006, 0x0000001f, 0x00000001, 0x505f6c67,
0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000001f, 0x00000002,
0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000001f,
0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005,
0x00000021, 0x00000000, 0x00040005, 0x00000024, 0x68737550, 0x00000000,
0x00070006, 0x00000024, 0x00000000, 0x77656976, 0x6a6f7250, 0x69746365,
0x00006e6f, 0x00040005, 0x00000026, 0x68737570, 0x00000000, 0x00040005,
0x0000002b, 0x65646f6d, 0x0000006c, 0x00050005, 0x00000035, 0x67617266,
0x6f6c6f43, 0x00000072, 0x00040005, 0x00000037, 0x6f6c6f63, 0x00000072,
0x00040005, 0x0000003a, 0x67617266, 0x00007655, 0x00040005, 0x0000003b,
0x65527675, 0x00007463, 0x00040047, 0x00000016, 0x0000000b, 0x0000002a,
0x00030047, 0x0000001f, 0x00000002, 0x00050048, 0x0000001f, 0x00000000,
0x0000000b, 0x00000000, 0x00050048, 0x0000001f, 0x00000001, 0x0000000b,
0x00000001, 0x00050048, 0x0000001f, 0x00000002, 0x0000000b, 0x00000003,
0x00050048, 0x0000001f, 0x00000003, 0x0000000b, 0x00000004, 0x00030047,
0x00000024, 0x00000002, 0x00040048, 0x00000024, 0x00000000, 0x00000005,
0x00050048, 0x00000024, 0x00000000, 0x00000007, 0x00000010, 0x00050048,
0x00000024, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000002b,
0x0000001e, 0x00000000, 0x00040047, 0x00000035, 0x0000001e, 0x00000000,
0x00040047, 0x00000037, 0x0000001e, 0x00000004, 0x00040047, 0x0000003a,
0x0000001e, 0x00000001, 0x00040047, 0x0000003b, 0x0000001e, 0x00000005,
0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000002,
0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x0000000a,
0x00000020, 0x00000000, 0x0004002b, 0x0000000a, 0x0000000b, 0x00000006,
0x0004001c, 0x0000000c, 0x00000007, 0x0000000b, 0x0004002b, 0x00000006,
0x0000000d, 0x00000000, 0x0005002c, 0x00000007, 0x0000000e, 0x0000000d,
0x0000000d, 0x0004002b, 0x00000006, 0x0000000f, 0x3f800000, 0x0005002c,
0x00000007, 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, 0x00000007,
0x00000011, 0x0000000f, 0x0000000f, 0x0005002c, 0x00000007, 0x00000012,
0x0000000d, 0x0000000f, 0x0009002c, 0x0000000c, 0x00000013, 0x0000000e,
0x00000010, 0x00000011, 0x00000011, 0x00000012, 0x0000000e, 0x00040015,
0x00000014, 0x00000020, 0x00000001, 0x00040020, 0x00000015, 0x00000001,
0x00000014, 0x0004003b, 0x00000015, 0x00000016, 0x00000001, 0x00040020,
0x00000018, 0x00000007, 0x0000000c, 0x00040017, 0x0000001c, 0x00000006,
0x00000004, 0x0004002b, 0x0000000a, 0x0000001d, 0x00000001, 0x0004001c,
0x0000001e, 0x00000006, 0x0000001d, 0x0006001e, 0x0000001f, 0x0000001c,
0x00000006, 0x0000001e, 0x0000001e, 0x00040020, 0x00000020, 0x00000003,
0x0000001f, 0x0004003b, 0x00000020, 0x00000021, 0x00000003, 0x0004002b,
0x00000014, 0x00000022, 0x00000000, 0x00040018, 0x00000023, 0x0000001c,
0x00000004, 0x0003001e, 0x00000024, 0x00000023, 0x00040020, 0x00000025,
0x00000009, 0x00000024, 0x0004003b, 0x00000025, 0x00000026, 0x00000009,
0x00040020, 0x00000027, 0x00000009, 0x00000023, 0x00040020, 0x0000002a,
0x00000001, 0x00000023, 0x0004003b, 0x0000002a, 0x0000002b, 0x00000001,
0x00040020, 0x00000033, 0x00000003, 0x0000001c, 0x0004003b, 0x00000033,
0x00000035, 0x00000003, 0x00040020, 0x00000036, 0x00000001, 0x0000001c,
0x0004003b, 0x00000036, 0x00000037, 0x00000001, 0x00040020, 0x00000039,
0x00000003, 0x00000007, 0x0004003b, 0x00000039, 0x0000003a, 0x00000003,
0x0004003b, 0x00000036, 0x0000003b, 0x00000001, 0x0004002b, 0x0000000a,
0x00000040, 0x00000000, 0x00040020, 0x00000041, 0x00000007, 0x00000006,
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8,
0x00000005, 0x0004003b, 0x00000008, 0x00000009, 0x00000007, 0x0004003b,
0x00000018, 0x00000019, 0x00000007, 0x0004003d, 0x00000014, 0x00000017,
0x00000016, 0x0003003e, 0x00000019, 0x00000013, 0x00050041, 0x00000008,
0x0000001a, 0x00000019, 0x00000017, 0x0004003d, 0x00000007, 0x0000001b,
0x0000001a, 0x0003003e, 0x00000009, 0x0000001b, 0x00050041, 0x00000027,
0x00000028, 0x00000026, 0x00000022, 0x0004003d, 0x00000023, 0x00000029,
0x00000028, 0x0004003d, 0x00000023, 0x0000002c, 0x0000002b, 0x00050092,
0x00000023, 0x0000002d, 0x00000029, 0x0000002c, 0x0004003d, 0x00000007,
0x0000002e, 0x00000009, 0x00050051, 0x00000006, 0x0000002f, 0x0000002e,
0x00000000, 0x00050051, 0x00000006, 0x00000030, 0x0000002e, 0x00000001,
0x00070050, 0x0000001c, 0x00000031, 0x0000002f, 0x00000030, 0x0000000d,
0x0000000f, 0x00050091, 0x0000001c, 0x00000032, 0x0000002d, 0x00000031,
0x00050041, 0x00000033, 0x00000034, 0x00000021, 0x00000022, 0x0003003e,
0x00000034, 0x00000032, 0x0004003d, 0x0000001c, 0x00000038, 0x00000037,
0x0003003e, 0x00000035, 0x00000038, 0x0004003d, 0x0000001c, 0x0000003c,
0x0000003b, 0x0007004f, 0x00000007, 0x0000003d, 0x0000003c, 0x0000003c,
0x00000000, 0x00000001, 0x0004003d, 0x0000001c, 0x0000003e, 0x0000003b,
0x0007004f, 0x00000007, 0x0000003f, 0x0000003e, 0x0000003e, 0x00000002,
0x00000003, 0x00050041, 0x00000041, 0x00000042, 0x00000009, 0x00000040,
0x0004003d, 0x00000006, 0x00000043, 0x00000042, 0x00050041, 0x00000041,
0x00000044, 0x00000009, 0x0000001d, 0x0004003d, 0x00000006, 0x00000045,
0x00000044, 0x00050083, 0x00000006, 0x00000046, 0x0000000f, 0x00000045,
0x00050050, 0x00000007, 0x00000047, 0x00000043, 0x00000046, 0x0008000c,
0x00000007, 0x00000048, 0x00000001, 0x0000002e, 0x0000003d, 0x0000003f,
0x00000047, 0x0003003e, 0x0000003a, 0x00000048, 0x000100fd, 0x00010038,
};
} // namespace sophia::shaders
+2 -1
View File
@@ -47,7 +47,8 @@ void Panel::draw(const FrameInfo& frameInfo) {
}
bool open = true;
ImGui::Begin(config.name.c_str(), config.hasCloseButton() ? &open : nullptr,
this->shown = ImGui::Begin(config.name.c_str(),
config.hasCloseButton() ? &open : nullptr,
config.getWindowFlags());
onUpdate(frameInfo);
ImGui::End();
+2
View File
@@ -1,4 +1,5 @@
#include <sophia/ui/panel_registry.hpp>
#include <tracy/Tracy.hpp>
namespace sophia {
@@ -13,6 +14,7 @@ u32 PanelRegistry::visibleCount() const {
}
void PanelRegistry::update(const FrameInfo& frameInfo) {
ZoneScoped;
pool.forEach([&frameInfo](Panel& panel) {
if (panel.isVisible()) {
panel.draw(frameInfo);
+1 -1
View File
@@ -1,7 +1,7 @@
#include <sophia/ui/ui.hpp>
#include <sophia/util/assert.hpp>
#include "rendering/imgui_renderer.hpp"
#include "renderer/imgui_renderer.hpp"
namespace sophia {
+47 -28
View File
@@ -14,10 +14,11 @@ constexpr std::array<Access, 7> ALL_ACCESSES = {
Access::Present,
};
constexpr vk::AccessFlags WRITE_ACCESSES =
vk::AccessFlagBits::eColorAttachmentWrite |
vk::AccessFlagBits::eDepthStencilAttachmentWrite |
vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite;
constexpr vk::AccessFlags2 WRITE_ACCESSES =
vk::AccessFlagBits2::eColorAttachmentWrite |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite |
vk::AccessFlagBits2::eShaderStorageWrite |
vk::AccessFlagBits2::eTransferWrite;
} // namespace
@@ -25,34 +26,37 @@ AccessState getAccessState(Access access) {
switch (access) {
case Access::ColorWrite:
return {vk::ImageLayout::eColorAttachmentOptimal,
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite};
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite};
case Access::DepthWrite:
return {vk::ImageLayout::eDepthStencilAttachmentOptimal,
vk::PipelineStageFlagBits::eEarlyFragmentTests |
vk::PipelineStageFlagBits::eLateFragmentTests,
vk::AccessFlagBits::eDepthStencilAttachmentRead |
vk::AccessFlagBits::eDepthStencilAttachmentWrite};
vk::PipelineStageFlagBits2::eEarlyFragmentTests |
vk::PipelineStageFlagBits2::eLateFragmentTests,
vk::AccessFlagBits2::eDepthStencilAttachmentRead |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite};
case Access::SampledRead:
return {vk::ImageLayout::eShaderReadOnlyOptimal,
vk::PipelineStageFlagBits::eFragmentShader,
vk::AccessFlagBits::eShaderRead};
vk::PipelineStageFlagBits2::eFragmentShader,
vk::AccessFlagBits2::eShaderSampledRead};
case Access::StorageReadWrite:
return {
vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader,
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite};
return {vk::ImageLayout::eGeneral,
vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderStorageRead |
vk::AccessFlagBits2::eShaderStorageWrite};
case Access::TransferSrc:
return {vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferRead};
vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead};
case Access::TransferDst:
return {vk::ImageLayout::eTransferDstOptimal,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite};
vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite};
case Access::Present:
// Presentation isn't a pipeline stage; the present waits on a semaphore
// instead, so the barrier into this state has nothing to block.
return {vk::ImageLayout::ePresentSrcKHR,
vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}};
vk::PipelineStageFlagBits2::eNone, vk::AccessFlags2{}};
}
log::fatal("Unknown Access value.");
@@ -71,7 +75,7 @@ AccessState getStateForLayout(vk::ImageLayout layout) {
}
}
log::fatal("Unsupported image layout:", vk::to_string(layout));
log::fatal("Unsupported image layout: {}", vk::to_string(layout));
throw std::runtime_error("Unsupported image layout.");
}
@@ -83,22 +87,37 @@ bool needsBarrier(const AccessState& from, const AccessState& to) {
return from.layout != to.layout || writesImage(from) || writesImage(to);
}
vk::ImageMemoryBarrier makeImageBarrier(vk::Image image,
vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to) {
vk::ImageMemoryBarrier barrier{};
vk::ImageMemoryBarrier2 barrier{};
barrier.srcStageMask = from.stage;
barrier.dstStageMask = to.stage;
// Only writes need to be made available; reads just need the execution
// dependency the stage masks already give.
barrier.srcAccessMask = from.access & WRITE_ACCESSES;
barrier.dstAccessMask = to.access;
barrier.oldLayout = from.layout;
barrier.newLayout = to.layout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = range;
// Only writes need to be made available; reads just need the execution
// dependency the stage masks already give.
barrier.srcAccessMask = from.access & WRITE_ACCESSES;
barrier.dstAccessMask = to.access;
return barrier;
}
void recordImageBarriers(
vk::CommandBuffer commandBuffer,
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers) {
if (barriers.empty()) {
return;
}
vk::DependencyInfo dependency{};
dependency.imageMemoryBarrierCount = barriers.size();
dependency.pImageMemoryBarriers = barriers.data();
commandBuffer.pipelineBarrier2(dependency);
}
} // namespace sophia
+13 -5
View File
@@ -25,8 +25,8 @@ enum class Access {
*/
struct AccessState {
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe;
vk::AccessFlags access;
vk::PipelineStageFlags2 stage = vk::PipelineStageFlagBits2::eNone;
vk::AccessFlags2 access;
};
/**
@@ -35,8 +35,8 @@ struct AccessState {
* just-acquired state has to use this as its source stage, otherwise the
* layout transition isn't ordered after the acquire.
*/
inline constexpr vk::PipelineStageFlags SWAPCHAIN_ACQUIRE_STAGE =
vk::PipelineStageFlagBits::eColorAttachmentOutput;
inline constexpr vk::PipelineStageFlags2 SWAPCHAIN_ACQUIRE_STAGE =
vk::PipelineStageFlagBits2::eColorAttachmentOutput;
AccessState getAccessState(Access access);
@@ -55,9 +55,17 @@ bool writesImage(const AccessState& state);
*/
bool needsBarrier(const AccessState& from, const AccessState& to);
vk::ImageMemoryBarrier makeImageBarrier(vk::Image image,
/**
* A barrier carrying its own stage masks, for recording with
* pipelineBarrier2. Barriers flushed together don't share stages.
*/
vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to);
void recordImageBarriers(
vk::CommandBuffer commandBuffer,
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers);
} // namespace sophia

Some files were not shown because too many files have changed in this diff Show More