commit cdb2c800ccf0f892d5f2531ec1148cc910c2561c Author: a dinosaur Date: Sat May 31 18:09:29 2025 +1000 c: Add lessons 1-10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41fd8d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +/build/ +/xcode/ +/build-*/ + +.idea/ +cmake-build-*/ + +.swiftpm/ +.build/ +xcuserdata/ +DerivedData/ +Package.resolved + +/out/ +/target/ +Cargo.lock + +.vs/ +.vscode/ +CMakeSettings.json + +Thumbs.db +.DS_Store +*.exe +*.dll +*.metallibsym diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ebd7345 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION "3.11" FATAL_ERROR) +project(NeHe-SDL_GPU LANGUAGES C) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") + +find_package(SDL3 REQUIRED CONFIG) + +add_subdirectory(src/c) diff --git a/cmake/modules/AddLesson.cmake b/cmake/modules/AddLesson.cmake new file mode 100644 index 0000000..d191435 --- /dev/null +++ b/cmake/modules/AddLesson.cmake @@ -0,0 +1,38 @@ +function (add_lesson target) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "SOURCES;SHADERS;DATA") + + add_executable(${target} MACOSX_BUNDLE WIN32 + application.c application.h + nehe.c nehe.h + matrix.c matrix.h) + set_property(TARGET ${target} PROPERTY C_STANDARD 99) + + target_compile_options(${target} PRIVATE + $<$:-Weverything -Wno-declaration-after-statement -Wno-padded -Wno-switch-enum -Wno-cast-qual> + $<$:-Wall -Wextra -pedantic> + $<$:/W4>) + target_compile_definitions(${target} PRIVATE + $<$:_CRT_SECURE_NO_WARNINGS>) + target_link_libraries(${target} SDL3::SDL3) + + target_sources(${target} PRIVATE ${arg_SOURCES}) + + foreach (shader IN LISTS arg_SHADERS) + if (shader MATCHES "\\.metal$") + set(path "${CMAKE_SOURCE_DIR}/src/shaders/${shader}") + else() + set(path "${CMAKE_SOURCE_DIR}/data/shaders/${shader}") + endif() + set_source_files_properties(${path} PROPERTIES + HEADER_FILE_ONLY ON + MACOSX_PACKAGE_LOCATION "Resources/Shaders") + target_sources(${target} PRIVATE "${path}") + endforeach() + foreach (resource IN LISTS arg_DATA) + set(path "${CMAKE_SOURCE_DIR}/data/${resource}") + set_source_files_properties(${path} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources/Data") + target_sources(${target} PRIVATE "${path}") + endforeach() + unset(path) +endfunction() diff --git a/data/Crate.bmp b/data/Crate.bmp new file mode 100644 index 0000000..73fe985 Binary files /dev/null and b/data/Crate.bmp differ diff --git a/data/Glass.bmp b/data/Glass.bmp new file mode 100644 index 0000000..1acf064 Binary files /dev/null and b/data/Glass.bmp differ diff --git a/data/Mud.bmp b/data/Mud.bmp new file mode 100644 index 0000000..9b10897 Binary files /dev/null and b/data/Mud.bmp differ diff --git a/data/NeHe.bmp b/data/NeHe.bmp new file mode 100644 index 0000000..d36bd34 Binary files /dev/null and b/data/NeHe.bmp differ diff --git a/data/Star.bmp b/data/Star.bmp new file mode 100644 index 0000000..15737cd Binary files /dev/null and b/data/Star.bmp differ diff --git a/data/World.txt b/data/World.txt new file mode 100644 index 0000000..767385e --- /dev/null +++ b/data/World.txt @@ -0,0 +1,160 @@ + +NUMPOLLIES 36 + +// Floor 1 +-3.0 0.0 -3.0 0.0 6.0 +-3.0 0.0 3.0 0.0 0.0 + 3.0 0.0 3.0 6.0 0.0 + +-3.0 0.0 -3.0 0.0 6.0 + 3.0 0.0 -3.0 6.0 6.0 + 3.0 0.0 3.0 6.0 0.0 + +// Ceiling 1 +-3.0 1.0 -3.0 0.0 6.0 +-3.0 1.0 3.0 0.0 0.0 + 3.0 1.0 3.0 6.0 0.0 +-3.0 1.0 -3.0 0.0 6.0 + 3.0 1.0 -3.0 6.0 6.0 + 3.0 1.0 3.0 6.0 0.0 + +// A1 + +-2.0 1.0 -2.0 0.0 1.0 +-2.0 0.0 -2.0 0.0 0.0 +-0.5 0.0 -2.0 1.5 0.0 +-2.0 1.0 -2.0 0.0 1.0 +-0.5 1.0 -2.0 1.5 1.0 +-0.5 0.0 -2.0 1.5 0.0 + +// A2 + + 2.0 1.0 -2.0 2.0 1.0 + 2.0 0.0 -2.0 2.0 0.0 + 0.5 0.0 -2.0 0.5 0.0 + 2.0 1.0 -2.0 2.0 1.0 + 0.5 1.0 -2.0 0.5 1.0 + 0.5 0.0 -2.0 0.5 0.0 + +// B1 + +-2.0 1.0 2.0 2.0 1.0 +-2.0 0.0 2.0 2.0 0.0 +-0.5 0.0 2.0 0.5 0.0 +-2.0 1.0 2.0 2.0 1.0 +-0.5 1.0 2.0 0.5 1.0 +-0.5 0.0 2.0 0.5 0.0 + +// B2 + + 2.0 1.0 2.0 2.0 1.0 + 2.0 0.0 2.0 2.0 0.0 + 0.5 0.0 2.0 0.5 0.0 + 2.0 1.0 2.0 2.0 1.0 + 0.5 1.0 2.0 0.5 1.0 + 0.5 0.0 2.0 0.5 0.0 + +// C1 + +-2.0 1.0 -2.0 0.0 1.0 +-2.0 0.0 -2.0 0.0 0.0 +-2.0 0.0 -0.5 1.5 0.0 +-2.0 1.0 -2.0 0.0 1.0 +-2.0 1.0 -0.5 1.5 1.0 +-2.0 0.0 -0.5 1.5 0.0 + +// C2 + +-2.0 1.0 2.0 2.0 1.0 +-2.0 0.0 2.0 2.0 0.0 +-2.0 0.0 0.5 0.5 0.0 +-2.0 1.0 2.0 2.0 1.0 +-2.0 1.0 0.5 0.5 1.0 +-2.0 0.0 0.5 0.5 0.0 + +// D1 + +2.0 1.0 -2.0 0.0 1.0 +2.0 0.0 -2.0 0.0 0.0 +2.0 0.0 -0.5 1.5 0.0 +2.0 1.0 -2.0 0.0 1.0 +2.0 1.0 -0.5 1.5 1.0 +2.0 0.0 -0.5 1.5 0.0 + +// D2 + +2.0 1.0 2.0 2.0 1.0 +2.0 0.0 2.0 2.0 0.0 +2.0 0.0 0.5 0.5 0.0 +2.0 1.0 2.0 2.0 1.0 +2.0 1.0 0.5 0.5 1.0 +2.0 0.0 0.5 0.5 0.0 + +// Upper hallway - L +-0.5 1.0 -3.0 0.0 1.0 +-0.5 0.0 -3.0 0.0 0.0 +-0.5 0.0 -2.0 1.0 0.0 +-0.5 1.0 -3.0 0.0 1.0 +-0.5 1.0 -2.0 1.0 1.0 +-0.5 0.0 -2.0 1.0 0.0 + +// Upper hallway - R +0.5 1.0 -3.0 0.0 1.0 +0.5 0.0 -3.0 0.0 0.0 +0.5 0.0 -2.0 1.0 0.0 +0.5 1.0 -3.0 0.0 1.0 +0.5 1.0 -2.0 1.0 1.0 +0.5 0.0 -2.0 1.0 0.0 + +// Lower hallway - L +-0.5 1.0 3.0 0.0 1.0 +-0.5 0.0 3.0 0.0 0.0 +-0.5 0.0 2.0 1.0 0.0 +-0.5 1.0 3.0 0.0 1.0 +-0.5 1.0 2.0 1.0 1.0 +-0.5 0.0 2.0 1.0 0.0 + +// Lower hallway - R +0.5 1.0 3.0 0.0 1.0 +0.5 0.0 3.0 0.0 0.0 +0.5 0.0 2.0 1.0 0.0 +0.5 1.0 3.0 0.0 1.0 +0.5 1.0 2.0 1.0 1.0 +0.5 0.0 2.0 1.0 0.0 + + +// Left hallway - Lw + +-3.0 1.0 0.5 1.0 1.0 +-3.0 0.0 0.5 1.0 0.0 +-2.0 0.0 0.5 0.0 0.0 +-3.0 1.0 0.5 1.0 1.0 +-2.0 1.0 0.5 0.0 1.0 +-2.0 0.0 0.5 0.0 0.0 + +// Left hallway - Hi + +-3.0 1.0 -0.5 1.0 1.0 +-3.0 0.0 -0.5 1.0 0.0 +-2.0 0.0 -0.5 0.0 0.0 +-3.0 1.0 -0.5 1.0 1.0 +-2.0 1.0 -0.5 0.0 1.0 +-2.0 0.0 -0.5 0.0 0.0 + +// Right hallway - Lw + +3.0 1.0 0.5 1.0 1.0 +3.0 0.0 0.5 1.0 0.0 +2.0 0.0 0.5 0.0 0.0 +3.0 1.0 0.5 1.0 1.0 +2.0 1.0 0.5 0.0 1.0 +2.0 0.0 0.5 0.0 0.0 + +// Right hallway - Hi + +3.0 1.0 -0.5 1.0 1.0 +3.0 0.0 -0.5 1.0 0.0 +2.0 0.0 -0.5 0.0 0.0 +3.0 1.0 -0.5 1.0 1.0 +2.0 1.0 -0.5 0.0 1.0 +2.0 0.0 -0.5 0.0 0.0 \ No newline at end of file diff --git a/data/shaders/lesson2.metallib b/data/shaders/lesson2.metallib new file mode 100644 index 0000000..0a3f31e Binary files /dev/null and b/data/shaders/lesson2.metallib differ diff --git a/data/shaders/lesson3.metallib b/data/shaders/lesson3.metallib new file mode 100644 index 0000000..0733a7b Binary files /dev/null and b/data/shaders/lesson3.metallib differ diff --git a/data/shaders/lesson6.metallib b/data/shaders/lesson6.metallib new file mode 100644 index 0000000..e5bd78f Binary files /dev/null and b/data/shaders/lesson6.metallib differ diff --git a/data/shaders/lesson7.metallib b/data/shaders/lesson7.metallib new file mode 100644 index 0000000..cb46b92 Binary files /dev/null and b/data/shaders/lesson7.metallib differ diff --git a/data/shaders/lesson9.metallib b/data/shaders/lesson9.metallib new file mode 100644 index 0000000..36ad95c Binary files /dev/null and b/data/shaders/lesson9.metallib differ diff --git a/scripts/compile_shaders.py b/scripts/compile_shaders.py new file mode 100755 index 0000000..32399f5 --- /dev/null +++ b/scripts/compile_shaders.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +SPDX-FileCopyrightText: (C) 2025 a dinosaur +SPDX-License-Identifier: Zlib +""" + +import os +import shutil +import sys +from collections import namedtuple +from pathlib import Path +import subprocess +import platform +from typing import Iterable + + +def compile_metal_shaders( + sources: list[str | Path], library: str | Path, + cflags: list[str] = None, sdk="macosx", debug: bool = False, cwd: Path | None = None) -> None: + """Build a Metal shader library from a list of Metal shaders + + :param sources: List of Metal shader source paths + :param library: Path to the Metal library to build + :param cflags: Optional list of additional compiler parameters + :param sdk: Name of the Xcode platform SDK to use (default: "macosx"), + can be "macosx", "iphoneos", "iphonesimulator", "appletvos", or "appletvsimulator" + :param debug: Generate a symbol file that can be used to debug the shader library + :param cwd: Optionally set the current working directory for the compiler + """ + if cflags is None: + cflags = [] + + def xcrun_find_program(name: str): + return subprocess.run(["xcrun", "-sdk", sdk, "-f", name], + capture_output=True, check=True).stdout.decode("utf-8").strip() + + # Find Metal compilers + metal = xcrun_find_program("metal") + if debug: + metal_dsymutil = xcrun_find_program("metal-dsymutil") + else: + metallib = xcrun_find_program("metallib") + + # Compile each source to an AIR (Apple Intermediate Representation) + cflags = cflags + ["-frecord-sources"] + air_objects = [f"{str(s).removesuffix('.metal')}.air" for s in sources] + for src, obj in zip(sources, air_objects): + subprocess.run([metal, *cflags, "-c", src, "-o", obj], cwd=cwd, check=True) + + try: + # Build the Metal library + if debug: + subprocess.run([metal, "-frecord-sources", "-o", library, *air_objects], cwd=cwd, check=True) + subprocess.run([metal_dsymutil, "-flat", "-remove-source", library], cwd=cwd, check=True) + else: + subprocess.run([metallib, "-o", library, *air_objects], cwd=cwd, check=True) + finally: + # Clean up AIR objects + for obj in air_objects: + cwd.joinpath(obj).unlink() + + +Shader = namedtuple("Shader", ["source", "type", "output"]) + + +def shaders_suffixes(shaders: list[Shader], + in_suffix: str | None, out_suffix: str | None) -> Iterable[Shader]: + """Add file extensions to the source and outputs of a list of shaders + + :param shaders: The list of Shader tuples + :param in_suffix: Optional file extension to append to source + :param out_suffix: Optional file extension to append to output + :return: Generator with the modified shaders + """ + for s in shaders: + yield Shader( + f"{s.source}.{in_suffix}" if in_suffix else s.source, + s.type, + f"{s.output}.{out_suffix}" if out_suffix else s.output) + + +def compile_spirv_shaders(shaders: Iterable[Shader], + flags: list[str] = None, glslang: str | None=None, cwd: Path | None = None) -> None: + """Compile shaders to SPIR-V using glslang + + :param shaders: The list of shader source paths to compile + :param flags: List of additional flags to pass to glslang + :param glslang: Optional path to glslang executable, if `None` defaults to "glslang" + :param cwd: Optionally set the current working directory for the compiler + """ + if glslang is None: + glslang = "glslang" + if flags is None: + flags = [] + + for shader in shaders: + sflags = [*flags, "-V", "-S", shader.type, "-o", shader.output, shader.source] + subprocess.run([glslang, *sflags], cwd=cwd, check=True) + + +def compile_dxil_shaders(shaders: Iterable[Shader], dxc: str | None = None, cwd: Path | None = None) -> None: + """Compile HLSL shaders to DXIL using DXC + + :param shaders: The list of shader source paths to compile + :param dxc: Optional path to dxc excutable, if `None` defaults to "dxc" + :param cwd: Optionally set the current working directory for the compiler + """ + if dxc is None: + dxc = "dxc" + for shader in shaders: + entry, shader_type = { + "vert": ("VertexMain", "vs_6_0"), + "frag": ("FragmentMain", "ps_6_0") }[shader.type] + cflags = ["-E", entry, "-T", shader_type] + subprocess.run([dxc, *cflags, "-Fo", shader.output, shader.source], cwd=cwd, check=True) + + +def compile_dxbc_shaders(shaders: Iterable[Shader], cwd: Path | None = None) -> None: + """Compile HLSL shaders to DXBC using FXC + + :param shaders: The list of shader source paths to compile + :param cwd: Optionally set the current working directory for the compiler + """ + for shader in shaders: + entry, shader_type = { + "vert": ("VertexMain", "vs_5_1"), + "frag": ("FragmentMain", "ps_5_1") }[shader.type] + cflags = ["/E", entry, "/T", shader_type] + subprocess.run(["fxc", *cflags, "/Fo", shader.output, shader.source], cwd=cwd, check=True) + + +def compile_shaders() -> None: + build_spirv = False + build_metal = True + build_dxil = False + build_dxbc = False + lessons = [ "lesson2", "lesson3", "lesson6", "lesson7", "lesson9" ] + src_dir = Path("src/shaders") + dest_dir = Path("data/shaders") + + system = platform.system() + def add_shaders() -> Iterable[Shader]: + for lesson in lessons: + yield Shader(src_dir / f"{lesson}.vertex", "vert", dest_dir / f"{lesson}.vertex") + yield Shader(src_dir / f"{lesson}.fragment", "frag", dest_dir / f"{lesson}.fragment") + shaders = list(add_shaders()) + + root = Path(sys.argv[0]).resolve().parent.parent + root.joinpath(dest_dir).mkdir(parents=True, exist_ok=True) + + # Try to find cross-platform shader compilers + glslang = shutil.which("glslang") + dxc = shutil.which("dxc", path=f"/opt/dxc/bin:{os.environ.get('PATH', os.defpath)}") + + if build_spirv: + # Build SPIR-V shaders for Vulkan + compile_spirv_shaders(shaders_suffixes(shaders, "glsl", "spv"), + flags=["--quiet"], glslang=glslang, cwd=root) + + if build_metal: + # Build Metal shaders on macOS + if system == "Darwin": + compile_platform = "macos" + sdk_platform = "macosx" + min_version = "10.11" + for lesson in lessons: + compile_metal_shaders( + sources=[src_dir / f"{lesson}.metal"], + library=dest_dir / f"{lesson}.metallib", + cflags=["-Wall", "-O3", + f"-std={compile_platform}-metal1.1", + f"-m{sdk_platform}-version-min={min_version}"], + sdk=sdk_platform, + cwd=root) + + if build_dxil: + # Build HLSL shaders on Windows or when DXC is available + if system == "Windows" or dxc is not None: + compile_dxil_shaders(shaders_suffixes(shaders, "hlsl", "dxb"), dxc=dxc, cwd=root) + if build_dxbc: + if system == "Windows": # FXC is only available thru the Windows SDK + compile_dxbc_shaders(shaders_suffixes(shaders, "hlsl", "fxb"), cwd=root) + + +if __name__ == "__main__": + compile_shaders() diff --git a/src/c/CMakeLists.txt b/src/c/CMakeLists.txt new file mode 100644 index 0000000..9f88703 --- /dev/null +++ b/src/c/CMakeLists.txt @@ -0,0 +1,12 @@ +include(AddLesson) + +add_lesson(lesson1 SOURCES lesson1.c) +add_lesson(lesson2 SOURCES lesson2.c SHADERS lesson2.metallib) +add_lesson(lesson3 SOURCES lesson3.c SHADERS lesson3.metallib) +add_lesson(lesson4 SOURCES lesson4.c SHADERS lesson3.metallib) +add_lesson(lesson5 SOURCES lesson5.c SHADERS lesson3.metallib) +add_lesson(lesson6 SOURCES lesson6.c SHADERS lesson6.metallib DATA NeHe.bmp) +add_lesson(lesson7 SOURCES lesson7.c SHADERS lesson6.metallib lesson7.metallib DATA Crate.bmp) +add_lesson(lesson8 SOURCES lesson8.c SHADERS lesson6.metallib lesson7.metallib DATA Glass.bmp) +add_lesson(lesson9 SOURCES lesson9.c SHADERS lesson6.metallib lesson9.metallib DATA Star.bmp) +add_lesson(lesson10 SOURCES lesson10.c SHADERS lesson6.metallib DATA Mud.bmp World.txt) diff --git a/src/c/application.c b/src/c/application.c new file mode 100644 index 0000000..52ad65e --- /dev/null +++ b/src/c/application.c @@ -0,0 +1,211 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" +#define SDL_MAIN_USE_CALLBACKS +#include + + +typedef struct +{ + NeHeContext ctx; + bool fullscreen; +} AppState; + +SDL_AppResult SDLCALL SDL_AppInit(void** appstate, int argc, char* argv[]) +{ + (void)argc; (void)argv; + + // Initialise SDL + if (!SDL_Init(SDL_INIT_VIDEO)) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + SDL_SetCurrentThreadPriority(SDL_THREAD_PRIORITY_HIGH); + + // Allocate application context + AppState* s = *appstate = SDL_malloc(sizeof(AppState)); + if (!s) + { + return SDL_APP_FAILURE; + } + *s = (AppState) + { + .ctx = (NeHeContext) + { + .window = NULL, + .device = NULL + }, + .fullscreen = false + }; + NeHeContext* ctx = &s->ctx; + ctx->baseDir = SDL_GetBasePath(); // Resources directory + + // Initialise GPU context + if (!NeHe_InitGPU(ctx, appConfig.title, appConfig.width, appConfig.height)) + { + return SDL_APP_FAILURE; + } + + // Handle depth buffer texture creation if requested + if (appConfig.createDepthFormat != SDL_GPU_TEXTUREFORMAT_INVALID) + { + unsigned backbufWidth, backbufHeight; + SDL_GetWindowSizeInPixels(ctx->window, (int*)&backbufWidth, (int*)&backbufHeight); + if (!NeHe_SetupDepthTexture(ctx, backbufWidth, backbufHeight, appConfig.createDepthFormat, 1.0f)) + { + return false; + } + } + + if (appConfig.init) + { + if (!appConfig.init(ctx)) + { + return SDL_APP_FAILURE; + } + } + + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDLCALL SDL_AppIterate(void* appstate) +{ + NeHeContext* ctx = &((AppState*)appstate)->ctx; + SDL_GPUCommandBuffer* cmdbuf = SDL_AcquireGPUCommandBuffer(ctx->device); + if (!cmdbuf) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AcquireGPUCommandBuffer: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + SDL_GPUTexture* swapchainTex = NULL; + uint32_t swapchainWidth, swapchainHeight; + if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmdbuf, ctx->window, &swapchainTex, &swapchainWidth, &swapchainHeight)) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_WaitAndAcquireGPUSwapchainTexture: %s", SDL_GetError()); + SDL_CancelGPUCommandBuffer(cmdbuf); + return SDL_APP_FAILURE; + } + if (!swapchainTex) + { + SDL_CancelGPUCommandBuffer(cmdbuf); + return SDL_APP_CONTINUE; + } + + if (appConfig.createDepthFormat != SDL_GPU_TEXTUREFORMAT_INVALID && ctx->depthTexture + && (ctx->depthTextureWidth != swapchainWidth || ctx->depthTextureHeight != swapchainHeight)) + { + if (!NeHe_SetupDepthTexture(ctx, swapchainWidth, swapchainHeight, appConfig.createDepthFormat, 1.0f)) + { + SDL_CancelGPUCommandBuffer(cmdbuf); + return SDL_APP_FAILURE; + } + } + + if (appConfig.draw) + { + appConfig.draw(ctx, cmdbuf, swapchainTex); + } + + SDL_SubmitGPUCommandBuffer(cmdbuf); + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) +{ + AppState* s = appstate; + + switch (event->type) + { + case SDL_EVENT_QUIT: + return SDL_APP_SUCCESS; + case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: + case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: + s->fullscreen = event->type == SDL_EVENT_WINDOW_ENTER_FULLSCREEN; + return SDL_APP_CONTINUE; + case SDL_EVENT_KEY_DOWN: + if (event->key.key == SDLK_ESCAPE) + { + return SDL_APP_SUCCESS; + } + if (event->key.key == SDLK_F1) + { + SDL_SetWindowFullscreen(s->ctx.window, !s->fullscreen); + return SDL_APP_CONTINUE; + } + // Fallthrough + case SDL_EVENT_KEY_UP: + if (appConfig.key) + { + appConfig.key(&s->ctx, event->key.key, event->key.down, event->key.repeat); + } + return SDL_APP_CONTINUE; + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + if (appConfig.resize) + { + appConfig.resize(&s->ctx, event->window.data1, event->window.data2); + } + return SDL_APP_CONTINUE; + default: + return SDL_APP_CONTINUE; + } +} + +void SDLCALL SDL_AppQuit(void* appstate, SDL_AppResult result) +{ + (void)result; + + if (appstate) + { + NeHeContext* ctx = &((AppState*)appstate)->ctx; + if (appConfig.quit) + { + appConfig.quit(ctx); + } + if (appConfig.createDepthFormat != SDL_GPU_TEXTUREFORMAT_INVALID && ctx->depthTexture) + { + SDL_ReleaseGPUTexture(ctx->device, ctx->depthTexture); + } + SDL_ReleaseWindowFromGPUDevice(ctx->device, ctx->window); + SDL_DestroyGPUDevice(ctx->device); + SDL_DestroyWindow(ctx->window); + SDL_free(ctx); + } + SDL_Quit(); +} + + +#ifndef SDL_MAIN_USE_CALLBACKS +int main(int argc, char* argv[]) +{ + void* appstate = NULL; + SDL_AppResult res; + if ((res = SDL_AppInit(&appstate, argc, argv)) != SDL_APP_CONTINUE) + { + goto Quit; + } + while (true) + { + SDL_Event event; + while (SDL_PollEvent(&event)) + { + if ((res = SDL_AppEvent(appstate, &event)) != SDL_APP_CONTINUE) + { + goto Quit; + } + } + if ((res = SDL_AppIterate(appstate)) != SDL_APP_CONTINUE) + { + goto Quit; + } + } +Quit: + SDL_AppQuit(appstate, res); + return res == SDL_APP_SUCCESS ? 0 : 1; +} +#endif diff --git a/src/c/application.h b/src/c/application.h new file mode 100644 index 0000000..b431084 --- /dev/null +++ b/src/c/application.h @@ -0,0 +1,22 @@ +#ifndef APPLICATION_H +#define APPLICATION_H + +#include +#include +#include + +struct NeHeContext; + +extern const struct AppConfig +{ + const char* const title; + int width, height; + SDL_GPUTextureFormat createDepthFormat; + bool (*init)(struct NeHeContext*); + void (*quit)(struct NeHeContext*); + void (*resize)(struct NeHeContext*, int width, int height); + void (*draw)(struct NeHeContext* restrict, SDL_GPUCommandBuffer* restrict, SDL_GPUTexture* restrict); + void (*key)(struct NeHeContext*, SDL_Keycode, bool down, bool repeat); +} appConfig; + +#endif//APPLICATION_H diff --git a/src/c/lesson1.c b/src/c/lesson1.c new file mode 100644 index 0000000..d9a9908 --- /dev/null +++ b/src/c/lesson1.c @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +static void Lesson1_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + (void)ctx; + + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, NULL); + SDL_EndGPURenderPass(pass); +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's OpenGL Framework", + .width = 640, .height = 480, + .init = NULL, + .quit = NULL, + .resize = NULL, + .draw = Lesson1_Draw +}; diff --git a/src/c/lesson10.c b/src/c/lesson10.c new file mode 100644 index 0000000..2428cd8 --- /dev/null +++ b/src/c/lesson10.c @@ -0,0 +1,392 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" +#include + + +typedef struct +{ + float x, y, z; + float u, v; +} Vertex; + +typedef struct +{ + Vertex vertices[3]; +} Triangle; + +typedef struct +{ + int numTriangles; + Triangle* tris; +} Sector; + +typedef struct +{ + float x, z; + float yaw, pitch; + float walkBob, walkBobTheta; +} Camera; + + +static SDL_GPUGraphicsPipeline* pso = NULL, * psoBlend = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUTexture* texture = NULL; +static SDL_GPUSampler* samplers[3] = { NULL, NULL, NULL }; + +static bool blend = false; +static unsigned filter = 0; + +static float projection[16]; +static Camera camera = +{ + .x = 0.0f, .z = 0.0f, + .yaw = 0.0f, .pitch = 0.0f, + .walkBob = 0.0f, .walkBobTheta = 0.0f +}; +static Sector world = { .numTriangles = 0, .tris = NULL }; + + +static void ReadLine(SDL_IOStream* restrict file, char* restrict str, int max) +{ + do + { + char* p = str; + for (int n = max - 1; n > 0; --n) + { + int8_t c; + if (!SDL_ReadS8(file, &c)) + break; + (*p++) = c; + if (c == '\n') + break; + } + (*p) = '\0'; + } while (str[0] == '/' || str[0] == '\n' || str[0] == '\r'); +} + +static void SetupWorld(const NeHeContext* ctx) +{ + SDL_IOStream* file = NeHe_OpenResource(ctx, "Data/World.txt", "r"); + if (!file) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to open \"%s\": %s", "Data/World.txt", SDL_GetError()); + return; + } + + int numTris; + char line[255]; + ReadLine(file, line, sizeof(line)); + SDL_sscanf(line, "NUMPOLLIES %d\n", &numTris); + + world.tris = SDL_malloc(sizeof(Triangle) * (size_t)numTris); + world.numTriangles = numTris; + for (int tri = 0; tri < numTris; ++tri) + { + for (int vtx = 0; vtx < 3; ++vtx) + { + ReadLine(file, line, sizeof(line)); + SDL_sscanf(line, "%f %f %f %f %f", + &world.tris[tri].vertices[vtx].x, + &world.tris[tri].vertices[vtx].y, + &world.tris[tri].vertices[vtx].z, + &world.tris[tri].vertices[vtx].u, + &world.tris[tri].vertices[vtx].v); + } + } +} + + +static bool Lesson10_Init(NeHeContext* ctx) +{ + SetupWorld(ctx); + + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson6", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1, .fragmentSamplers = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + .offset = offsetof(Vertex, u) + } + }; + SDL_GPUGraphicsPipelineCreateInfo info = + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE // Right-handed coordinates + }, + .target_info = + { + .num_color_targets = 1 + } + }; + + // Setup blend pipeline + const SDL_GPUTextureFormat swapchainTextureFormat = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window); + info.target_info.color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = swapchainTextureFormat, + .blend_state = + { + .enable_blend = true, + .color_blend_op = SDL_GPU_BLENDOP_ADD, + .alpha_blend_op = SDL_GPU_BLENDOP_ADD, + .src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA, + .dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE, + .src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA, + .dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE + } + }; + psoBlend = SDL_CreateGPUGraphicsPipeline(ctx->device, &info); + if (!psoBlend) + { + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + // Setup regular pipeline w/ depth testing + info.depth_stencil_state = (SDL_GPUDepthStencilState) + { + .compare_op = SDL_GPU_COMPAREOP_LESS, // Pass if pixel depth value tests less than the depth buffer value + .enable_depth_test = true, // Enable depth testing + .enable_depth_write = true + }; + info.target_info.color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = swapchainTextureFormat + }; + info.target_info.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM; + info.target_info.has_depth_stencil_target = true; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &info); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if ((texture = NeHe_LoadTexture(ctx, "Data/Mud.bmp", true, true)) == NULL) + { + return false; + } + + samplers[0] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_NEAREST, + .mag_filter = SDL_GPU_FILTER_NEAREST + }); + samplers[1] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR + }); + samplers[2] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR, + .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, + .max_lod = FLT_MAX + }); + if (!samplers[0] || !samplers[1] || !samplers[2]) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUSampler: %s", SDL_GetError()); + return false; + } + + if ((vtxBuffer = NeHe_CreateVertexBuffer(ctx, world.tris, sizeof(Triangle) * (uint32_t)world.numTriangles)) == NULL) + { + return false; + } + + return true; +} + +static void Lesson10_Quit(NeHeContext* ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + for (int i = SDL_arraysize(samplers) - 1; i > 0; --i) + { + SDL_ReleaseGPUSampler(ctx->device, samplers[i]); + } + SDL_ReleaseGPUTexture(ctx->device, texture); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoBlend); + SDL_free(world.tris); +} + +static void Lesson10_Resize(NeHeContext* ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson10_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.0f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + const SDL_GPUDepthStencilTargetInfo depthInfo = + { + .texture = ctx->depthTexture, + .clear_depth = 1.0f, // Ensure depth buffer clears to furthest value + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_DONT_CARE, + .stencil_load_op = SDL_GPU_LOADOP_DONT_CARE, + .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, + .cycle = true + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, blend ? psoBlend : pso); + + // Bind texture + SDL_BindGPUFragmentSamplers(pass, 0, &(SDL_GPUTextureSamplerBinding) + { + .texture = texture, + .sampler = samplers[filter] + }, 1); + + // Bind world vertex buffer + SDL_BindGPUVertexBuffers(pass, 0, &(SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + + // Setup the camera view matrix + float modelView[16]; + Mtx_Rotation(modelView, camera.pitch, 1.0f, 0.0f, 0.0f); + Mtx_Rotate(modelView, 360.0f - camera.yaw, 0.0f, 1.0f, 0.0f); + Mtx_Translate(modelView, -camera.x, -(0.25f + camera.walkBob), -camera.z); + + // Push shader uniforms + struct { float modelViewProj[16], color[4]; } u; + Mtx_Multiply(u.modelViewProj, projection, modelView); + SDL_memcpy(u.color, (float[4]){ 1.0f, 1.0f, 1.0f, 1.0f }, sizeof(float) * 4); + SDL_PushGPUVertexUniformData(cmd, 0, &u, sizeof(u)); + + // Draw world + SDL_DrawGPUPrimitives(pass, 3u * (uint32_t)world.numTriangles, 1, 0, 0); + + SDL_EndGPURenderPass(pass); + + // Handle keyboard input + const bool *keys = SDL_GetKeyboardState(NULL); + + const float piover180 = 0.0174532925f; + + if (keys[SDL_SCANCODE_UP]) + { + camera.x -= SDL_sinf(camera.yaw * piover180) * 0.05f; + camera.z -= SDL_cosf(camera.yaw * piover180) * 0.05f; + if (camera.walkBobTheta >= 359.0f) + { + camera.walkBobTheta = 0.0f; + } + else + { + camera.walkBobTheta += 10.0f; + } + camera.walkBob = SDL_sinf(camera.walkBobTheta * piover180) / 20.0f; + } + + if (keys[SDL_SCANCODE_DOWN]) + { + camera.x += SDL_sinf(camera.yaw * piover180) * 0.05f; + camera.z += SDL_cosf(camera.yaw * piover180) * 0.05f; + if (camera.walkBobTheta <= 1.0f) + { + camera.walkBobTheta = 359.0f; + } + else + { + camera.walkBobTheta -= 10.0f; + } + camera.walkBob = SDL_sinf(camera.walkBobTheta * piover180) / 20.0f; + } + + if (keys[SDL_SCANCODE_LEFT]) { camera.yaw += 1.0f; } + if (keys[SDL_SCANCODE_RIGHT]) { camera.yaw -= 1.0f; } + if (keys[SDL_SCANCODE_PAGEUP]) { camera.pitch -= 1.0f; } + if (keys[SDL_SCANCODE_PAGEDOWN]) { camera.pitch += 1.0f; } +} + +static void Lesson10_Key(NeHeContext* ctx, SDL_Keycode key, bool down, bool repeat) +{ + (void)ctx; + + if (down && !repeat) + { + switch (key) + { + case SDLK_B: + blend = !blend; + break; + case SDLK_F: + filter = (filter + 1) % SDL_arraysize(samplers); + break; + default: + break; + } + } +} + + +const struct AppConfig appConfig = +{ + .title = "Lionel Brits & NeHe's 3D World Tutorial", + .width = 640, .height = 480, + .createDepthFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM, + .init = Lesson10_Init, + .quit = Lesson10_Quit, + .resize = Lesson10_Resize, + .draw = Lesson10_Draw, + .key = Lesson10_Key +}; diff --git a/src/c/lesson2.c b/src/c/lesson2.c new file mode 100644 index 0000000..53e5995 --- /dev/null +++ b/src/c/lesson2.c @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +typedef struct +{ + float x, y, z; +} Vertex; + +static const Vertex vertices[] = +{ + // Triangle + { 0.0f, 1.0f, 0.0f }, // Top + { -1.0f, -1.0f, 0.0f }, // Bottom left + { 1.0f, -1.0f, 0.0f }, // Bottom right + // Quad + { -1.0f, 1.0f, 0.0f }, // Top left + { 1.0f, 1.0f, 0.0f }, // Top right + { 1.0f, -1.0f, 0.0f }, // Bottom right + { -1.0f, -1.0f, 0.0f } // Bottom left +}; + +static const uint16_t indices[] = +{ + // Triangle + 0, 1, 2, + // Quad + 3, 4, 5, 5, 6, 3 +}; + + +static SDL_GPUGraphicsPipeline* pso = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; + +static float projection[16]; + + +static bool Lesson2_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson2", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + } + }; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }, + .target_info = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window) + }, + .num_color_targets = 1, + } + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson2_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); +} + +static void Lesson2_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson2_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + (void)ctx; + + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, NULL); + SDL_BindGPUGraphicsPipeline(pass, pso); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + float model[16], viewproj[16]; + + // Draw triangle 1.5 units to the left and 6 units into the camera + Mtx_Translation(model, -1.5f, 0.0f, -6.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0); + + // Move to the right by 3 units and draw quad + Mtx_Translate(model, 3.0f, 0.0f, 0.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0); + + SDL_EndGPURenderPass(pass); +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's First Polygon Tutorial", + .width = 640, .height = 480, + .init = Lesson2_Init, + .quit = Lesson2_Quit, + .resize = Lesson2_Resize, + .draw = Lesson2_Draw +}; diff --git a/src/c/lesson3.c b/src/c/lesson3.c new file mode 100644 index 0000000..7a152cf --- /dev/null +++ b/src/c/lesson3.c @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +typedef struct +{ + float x, y, z; + float r, g, b, a; +} Vertex; + +static const Vertex vertices[] = +{ + // Triangle + { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Top (red) + { -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Bottom left (green) + { 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Bottom right (blue) + // Quad + { -1.0f, 1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f }, // Top left + { 1.0f, 1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f }, // Top right + { 1.0f, -1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f }, // Bottom right + { -1.0f, -1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f } // Bottom left +}; + +static const uint16_t indices[] = +{ + // Triangle + 0, 1, 2, + // Quad + 3, 4, 5, 5, 6, 3 +}; + + +static SDL_GPUGraphicsPipeline* pso = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; + +static float projection[16]; + + +static bool Lesson3_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson3", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, + .offset = offsetof(Vertex, r) + } + }; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }, + .target_info = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window) + }, + .num_color_targets = 1, + } + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson3_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); +} + +static void Lesson3_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson3_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + (void)ctx; + + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, NULL); + SDL_BindGPUGraphicsPipeline(pass, pso); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + float model[16], viewproj[16]; + + // Draw triangle 1.5 units to the left and 6 units into the camera + Mtx_Translation(model, -1.5f, 0.0f, -6.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0); + + // Move to the right by 3 units and draw quad + Mtx_Translate(model, 3.0f, 0.0f, 0.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0); + + SDL_EndGPURenderPass(pass); +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's Color Tutorial", + .width = 640, .height = 480, + .init = Lesson3_Init, + .quit = Lesson3_Quit, + .resize = Lesson3_Resize, + .draw = Lesson3_Draw +}; diff --git a/src/c/lesson4.c b/src/c/lesson4.c new file mode 100644 index 0000000..18c0917 --- /dev/null +++ b/src/c/lesson4.c @@ -0,0 +1,197 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +typedef struct +{ + float x, y, z; + float r, g, b, a; +} Vertex; + +static const Vertex vertices[] = +{ + // Triangle + { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Top (red) + { -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Bottom left (green) + { 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Bottom right (blue) + // Quad + { -1.0f, 1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f }, // Top left + { 1.0f, 1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f }, // Top right + { 1.0f, -1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f }, // Bottom right + { -1.0f, -1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f } // Bottom left +}; + +static const uint16_t indices[] = +{ + // Triangle + 0, 1, 2, + // Quad + 3, 4, 5, 5, 6, 3 +}; + + +static SDL_GPUGraphicsPipeline* pso = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; + +static float projection[16]; + +static float rotTri = 0.0f; +static float rotQuad = 0.0f; + + +static bool Lesson4_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson3", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, + .offset = offsetof(Vertex, r) + } + }; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }, + .target_info = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window) + }, + .num_color_targets = 1, + } + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson4_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); +} + +static void Lesson4_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson4_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + (void)ctx; + + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, NULL); + SDL_BindGPUGraphicsPipeline(pass, pso); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + float model[16], viewproj[16]; + + // Draw triangle 1.5 units to the left and 6 units into the camera + Mtx_Translation(model, -1.5f, 0.0f, -6.0f); + Mtx_Rotate(model, rotTri, 0.0f, 1.0f, 0.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0); + + // Draw quad 1.5 units to the right and 6 units into the camera + Mtx_Translation(model, 1.5f, 0.0f, -6.0f); + Mtx_Rotate(model, rotQuad, 1.0f, 0.0f, 0.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0); + + SDL_EndGPURenderPass(pass); + + rotTri += 0.2f; + rotQuad -= 0.15f; +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's Rotation Tutorial", + .width = 640, .height = 480, + .init = Lesson4_Init, + .quit = Lesson4_Quit, + .resize = Lesson4_Resize, + .draw = Lesson4_Draw +}; diff --git a/src/c/lesson5.c b/src/c/lesson5.c new file mode 100644 index 0000000..62ca955 --- /dev/null +++ b/src/c/lesson5.c @@ -0,0 +1,245 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +typedef struct +{ + float x, y, z; + float r, g, b, a; +} Vertex; + +static const Vertex vertices[] = +{ + // Pyramid + { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Top of pyramid (Red) + { -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Front-left of pyramid (Green) + { 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Front-right of pyramid (Blue) + { 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Back-right of pyramid (Green) + { -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Back-left of pyramid (Blue) + // Cube + { 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Top-right of top face (Green) + { -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Top-left of top face (Green) + { -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Bottom-left of top face (Green) + { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f }, // Bottom-right of top face (Green) + { 1.0f, -1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f }, // Top-right of bottom face (Orange) + { -1.0f, -1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f }, // Top-left of bottom face (Orange) + { -1.0f, -1.0f, -1.0f, 1.0f, 0.5f, 0.0f, 1.0f }, // Bottom-left of bottom face (Orange) + { 1.0f, -1.0f, -1.0f, 1.0f, 0.5f, 0.0f, 1.0f }, // Bottom-right of bottom face (Orange) + { 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Top-right of front face (Red) + { -1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Top-left of front face (Red) + { -1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Bottom-left of front face (Red) + { 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f }, // Bottom-right of front face (Red) + { 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f }, // Top-right of back face (Yellow) + { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f }, // Top-left of back face (Yellow) + { -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f }, // Bottom-left of back face (Yellow) + { 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f }, // Bottom-right of back face (Yellow) + { -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Top-right of left face (Blue) + { -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Top-left of left face (Blue) + { -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Bottom-left of left face (Blue) + { -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // Bottom-right of left face (Blue) + { 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f }, // Top-right of right face (Violet) + { 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }, // Top-left of right face (Violet) + { 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }, // Bottom-left of right face (Violet) + { 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f } // Bottom-right of right face (Violet) +}; + +static const uint16_t indices[] = +{ + // Pyramid + 0, 1, 2, // Front + 0, 2, 3, // Right + 0, 3, 4, // Back + 0, 4, 1, // Left + // Cube + 5, 6, 7, 7, 8, 5, // Top + 9, 10, 11, 11, 12, 9, // Bottom + 13, 14, 15, 15, 16, 13, // Front + 17, 18, 19, 19, 20, 17, // Back + 21, 22, 23, 23, 24, 21, // Left + 25, 26, 27, 27, 28, 25 // Right +}; + + +static SDL_GPUGraphicsPipeline* pso = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; + +static float projection[16]; + +static float rotTri = 0.0f; +static float rotQuad = 0.0f; + + +static bool Lesson5_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson3", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, + .offset = offsetof(Vertex, r) + } + }; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }, + .depth_stencil_state = + { + .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, + .enable_depth_test = true, + .enable_depth_write = true + }, + .target_info = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window) + }, + .num_color_targets = 1, + .depth_stencil_format = appConfig.createDepthFormat, + .has_depth_stencil_target = true + } + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson5_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); +} + +static void Lesson5_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson5_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + const SDL_GPUDepthStencilTargetInfo depthInfo = + { + .texture = ctx->depthTexture, + .clear_depth = 1.0f, // Ensure depth buffer clears to furthest value + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_DONT_CARE, + .stencil_load_op = SDL_GPU_LOADOP_DONT_CARE, + .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, + .cycle = true + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, pso); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + float model[16], viewproj[16]; + + // Draw triangle 1.5 units to the left and 6 units into the camera + Mtx_Translation(model, -1.5f, 0.0f, -6.0f); + Mtx_Rotate(model, rotTri, 0.0f, 1.0f, 0.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 12, 1, 0, 0, 0); + + // Draw quad 1.5 units to the right and 7 units into the camera + Mtx_Translation(model, 1.5f, 0.0f, -7.0f); + Mtx_Rotate(model, rotQuad, 1.0f, 1.0f, 1.0f); + Mtx_Multiply(viewproj, projection, model); + SDL_PushGPUVertexUniformData(cmd, 0, viewproj, sizeof(viewproj)); + SDL_DrawGPUIndexedPrimitives(pass, 36, 1, 12, 0, 0); + + SDL_EndGPURenderPass(pass); + + rotTri += 0.2f; + rotQuad -= 0.15f; +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's Solid Object Tutorial", + .width = 640, .height = 480, + .createDepthFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM, + .init = Lesson5_Init, + .quit = Lesson5_Quit, + .resize = Lesson5_Resize, + .draw = Lesson5_Draw +}; diff --git a/src/c/lesson6.c b/src/c/lesson6.c new file mode 100644 index 0000000..5b91892 --- /dev/null +++ b/src/c/lesson6.c @@ -0,0 +1,266 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +typedef struct +{ + float x, y, z; + float u, v; +} Vertex; + +static const Vertex vertices[] = +{ + // Front Face + { -1.0f, -1.0f, 1.0f, 0.0f, 0.0f }, + { 1.0f, -1.0f, 1.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, 1.0f, 0.0f, 1.0f }, + // Back Face + { -1.0f, -1.0f, -1.0f, 1.0f, 0.0f }, + { -1.0f, 1.0f, -1.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, -1.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, -1.0f, 0.0f, 0.0f }, + // Top Face + { -1.0f, 1.0f, -1.0f, 0.0f, 1.0f }, + { -1.0f, 1.0f, 1.0f, 0.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, -1.0f, 1.0f, 1.0f }, + // Bottom Face + { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f }, + { 1.0f, -1.0f, -1.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f, 0.0f, 0.0f }, + { -1.0f, -1.0f, 1.0f, 1.0f, 0.0f }, + // Right face + { 1.0f, -1.0f, -1.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, -1.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f, 0.0f, 0.0f }, + // Left Face + { -1.0f, -1.0f, -1.0f, 0.0f, 0.0f }, + { -1.0f, -1.0f, 1.0f, 1.0f, 0.0f }, + { -1.0f, 1.0f, 1.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, -1.0f, 0.0f, 1.0f } +}; + +static const uint16_t indices[] = +{ + 0, 1, 2, 2, 3, 0, // Front + 4, 5, 6, 6, 7, 4, // Back + 8, 9, 10, 10, 11, 8, // Top + 12, 13, 14, 14, 15, 12, // Bottom + 16, 17, 18, 18, 19, 16, // Right + 20, 21, 22, 22, 23, 20 // Left +}; + + +static SDL_GPUGraphicsPipeline* pso = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; +static SDL_GPUSampler* sampler = NULL; +static SDL_GPUTexture* texture = NULL; + +static float projection[16]; + +static float xRot = 0.0f, yRot = 0.0f, zRot = 0.0f; + + +static bool Lesson6_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson6", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1, .fragmentSamplers = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + .offset = offsetof(Vertex, u) + } + }; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }, + .depth_stencil_state = + { + .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, + .enable_depth_test = true, + .enable_depth_write = true + }, + .target_info = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window) + }, + .num_color_targets = 1, + .depth_stencil_format = appConfig.createDepthFormat, + .has_depth_stencil_target = true + } + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if ((texture = NeHe_LoadTexture(ctx, "Data/NeHe.bmp", true, false)) == NULL) + { + return false; + } + + sampler = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR + }); + if (!sampler) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUSampler: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson6_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + SDL_ReleaseGPUSampler(ctx->device, sampler); + SDL_ReleaseGPUTexture(ctx->device, texture); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); +} + +static void Lesson6_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson6_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + const SDL_GPUDepthStencilTargetInfo depthInfo = + { + .texture = ctx->depthTexture, + .clear_depth = 1.0f, // Ensure depth buffer clears to furthest value + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_DONT_CARE, + .stencil_load_op = SDL_GPU_LOADOP_DONT_CARE, + .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, + .cycle = true + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, pso); + + // Bind texture + SDL_BindGPUFragmentSamplers(pass, 0, &(const SDL_GPUTextureSamplerBinding) + { + .texture = texture, + .sampler = sampler + }, 1); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + float model[16]; + struct { float modelViewProj[16], color[4]; } u; + + // Move cube 5 units into the screen and apply some rotations + Mtx_Translation(model, 0.0f, 0.0f, -5.0f); + Mtx_Rotate(model, xRot, 1.0f, 0.0f, 0.0f); + Mtx_Rotate(model, yRot, 0.0f, 1.0f, 0.0f); + Mtx_Rotate(model, zRot, 0.0f, 0.0f, 1.0f); + + // Push shader uniforms + Mtx_Multiply(u.modelViewProj, projection, model); + SDL_memcpy(u.color, (float[4]){ 1.0f, 1.0f, 1.0f, 1.0f }, sizeof(float) * 4); + SDL_PushGPUVertexUniformData(cmd, 0, &u, sizeof(u)); + + // Draw textured cube + SDL_DrawGPUIndexedPrimitives(pass, SDL_arraysize(indices), 1, 0, 0, 0); + + SDL_EndGPURenderPass(pass); + + xRot += 0.3f; + yRot += 0.2f; + zRot += 0.4f; +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's Texture Mapping Tutorial", + .width = 640, .height = 480, + .createDepthFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM, + .init = Lesson6_Init, + .quit = Lesson6_Quit, + .resize = Lesson6_Resize, + .draw = Lesson6_Draw +}; diff --git a/src/c/lesson7.c b/src/c/lesson7.c new file mode 100644 index 0000000..26df537 --- /dev/null +++ b/src/c/lesson7.c @@ -0,0 +1,377 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" +#include + + +typedef struct +{ + float x, y, z; + float nx, ny, nz; + float u, v; +} Vertex; + +static const Vertex vertices[] = +{ + // Front Face + { -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }, + { 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f }, + // Back Face + { -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f }, + { -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f }, + // Top Face + { -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }, + { -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f }, + // Bottom Face + { -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f }, + { 1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f }, + { -1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f }, + // Right face + { 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f }, + // Left Face + { -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f }, + { -1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f }, + { -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f } +}; + +static const uint16_t indices[] = +{ + 0, 1, 2, 2, 3, 0, // Front + 4, 5, 6, 6, 7, 4, // Back + 8, 9, 10, 10, 11, 8, // Top + 12, 13, 14, 14, 15, 12, // Bottom + 16, 17, 18, 18, 19, 16, // Right + 20, 21, 22, 22, 23, 20 // Left +}; + + +static SDL_GPUGraphicsPipeline* psoUnlit = NULL, * psoLight = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; +static SDL_GPUSampler* samplers[3] = { NULL, NULL, NULL }; +static SDL_GPUTexture* texture = NULL; + +static float projection[16]; + +static bool lighting = false; +struct Light { float ambient[4], diffuse[4], position[4]; } static light = +{ + .ambient = { 0.5f, 0.5f, 0.5f, 1.0f }, + .diffuse = { 1.0f, 1.0f, 1.0f, 1.0f }, + .position = { 0.0f, 0.0f, 2.0f, 1.0f } +}; + +static int filter = 0; + +static float xRot = 0.0f, yRot = 0.0f; +static float xSpeed = 0.0f, ySpeed = 0.0f; +static float z = -5.0f; + + +static bool Lesson7_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShaderUnlit, * fragmentShaderUnlit; + SDL_GPUShader* vertexShaderLight, * fragmentShaderLight; + if (!NeHe_LoadShaders(ctx, &vertexShaderUnlit, &fragmentShaderUnlit, "lesson6", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1, .fragmentSamplers = 1 })) + { + return false; + } + if (!NeHe_LoadShaders(ctx, &vertexShaderLight, &fragmentShaderLight, "lesson7", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 2, .fragmentSamplers = 1 })) + { + SDL_ReleaseGPUShader(ctx->device, fragmentShaderUnlit); + SDL_ReleaseGPUShader(ctx->device, vertexShaderUnlit); + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + .offset = offsetof(Vertex, u) + }, + { + .location = 2, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, nx) + } + }; + + const SDL_GPUVertexInputState vertexInput = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }; + + const SDL_GPURasterizerState rasterizer = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }; + + const SDL_GPUDepthStencilState depthStencil = + { + .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, + .enable_depth_test = true, + .enable_depth_write = true + }; + + const SDL_GPUGraphicsPipelineTargetInfo targetInfo = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window) + }, + .num_color_targets = 1, + .depth_stencil_format = appConfig.createDepthFormat, + .has_depth_stencil_target = true + }; + + psoUnlit = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShaderUnlit, + .fragment_shader = fragmentShaderUnlit, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = vertexInput, + .rasterizer_state = rasterizer, + .depth_stencil_state = depthStencil, + .target_info = targetInfo + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShaderUnlit); + SDL_ReleaseGPUShader(ctx->device, vertexShaderUnlit); + if (!psoUnlit) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + SDL_ReleaseGPUShader(ctx->device, fragmentShaderLight); + SDL_ReleaseGPUShader(ctx->device, vertexShaderLight); + return false; + } + + psoLight = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShaderLight, + .fragment_shader = fragmentShaderLight, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = vertexInput, + .rasterizer_state = rasterizer, + .depth_stencil_state = depthStencil, + .target_info = targetInfo + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShaderLight); + SDL_ReleaseGPUShader(ctx->device, vertexShaderLight); + if (!psoUnlit) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if ((texture = NeHe_LoadTexture(ctx, "Data/Crate.bmp", true, true)) == NULL) + { + return false; + } + + samplers[0] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_NEAREST, + .mag_filter = SDL_GPU_FILTER_NEAREST + }); + samplers[1] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR + }); + samplers[2] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR, + .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, + .max_lod = FLT_MAX + }); + if (!samplers[0] || !samplers[1] || !samplers[2]) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUSampler: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson7_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + for (int i = SDL_arraysize(samplers) - 1; i > 0; --i) + { + SDL_ReleaseGPUSampler(ctx->device, samplers[i]); + } + SDL_ReleaseGPUTexture(ctx->device, texture); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoLight); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoUnlit); +} + +static void Lesson7_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson7_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + const SDL_GPUDepthStencilTargetInfo depthInfo = + { + .texture = ctx->depthTexture, + .clear_depth = 1.0f, // Ensure depth buffer clears to furthest value + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_DONT_CARE, + .stencil_load_op = SDL_GPU_LOADOP_DONT_CARE, + .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, + .cycle = true + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, lighting ? psoLight : psoUnlit); + + // Bind texture + SDL_BindGPUFragmentSamplers(pass, 0, &(const SDL_GPUTextureSamplerBinding) + { + .texture = texture, + .sampler = samplers[filter] + }, 1); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + // Setup the cube's model matrix + float model[16]; + Mtx_Translation(model, 0.0f, 0.0f, z); + Mtx_Rotate(model, xRot, 1.0f, 0.0f, 0.0f); + Mtx_Rotate(model, yRot, 0.0f, 1.0f, 0.0f); + + // Push shader uniforms + if (lighting) + { + struct { float model[16], projection[16]; } u; + SDL_memcpy(u.model, model, sizeof(u.model)); + SDL_memcpy(u.projection, projection, sizeof(u.projection)); + SDL_PushGPUVertexUniformData(cmd, 0, &u, sizeof(u)); + SDL_PushGPUVertexUniformData(cmd, 1, &light, sizeof(light)); + } + else + { + struct { float modelViewProj[16], color[4]; } u; + Mtx_Multiply(u.modelViewProj, projection, model); + SDL_memcpy(u.color, (const float[4]){ 1.0f, 1.0f, 1.0f, 1.0f }, sizeof(float) * 4); + SDL_PushGPUVertexUniformData(cmd, 0, &u, sizeof(u)); + } + + // Draw textured cube + SDL_DrawGPUIndexedPrimitives(pass, SDL_arraysize(indices), 1, 0, 0, 0); + + SDL_EndGPURenderPass(pass); + + const bool* keys = SDL_GetKeyboardState(NULL); + + if (keys[SDL_SCANCODE_PAGEUP]) { z -= 0.02f; } + if (keys[SDL_SCANCODE_PAGEDOWN]) { z += 0.02f; } + if (keys[SDL_SCANCODE_UP]) { xSpeed -= 0.01f; } + if (keys[SDL_SCANCODE_DOWN]) { xSpeed += 0.01f; } + if (keys[SDL_SCANCODE_RIGHT]) { ySpeed += 0.1f; } + if (keys[SDL_SCANCODE_LEFT]) { ySpeed -= 0.1f; } + + xRot += xSpeed; + yRot += ySpeed; +} + +static void Lesson7_Key(NeHeContext* ctx, SDL_Keycode key, bool down, bool repeat) +{ + (void)ctx; + + if (down && !repeat) + { + switch (key) + { + case SDLK_L: + lighting = !lighting; + break; + case SDLK_F: + filter = (filter + 1) % (int)SDL_arraysize(samplers); + break; + default: + break; + } + } +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's Textures, Lighting & Keyboard Tutorial", + .width = 640, .height = 480, + .createDepthFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM, + .init = Lesson7_Init, + .quit = Lesson7_Quit, + .resize = Lesson7_Resize, + .draw = Lesson7_Draw, + .key = Lesson7_Key +}; diff --git a/src/c/lesson8.c b/src/c/lesson8.c new file mode 100644 index 0000000..5b9ae68 --- /dev/null +++ b/src/c/lesson8.c @@ -0,0 +1,400 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" +#include + + +typedef struct +{ + float x, y, z; + float nx, ny, nz; + float u, v; +} Vertex; + +static const Vertex vertices[] = +{ + // Front Face + { -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }, + { 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f }, + // Back Face + { -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f }, + { -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f }, + // Top Face + { -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f }, + { -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f }, + // Bottom Face + { -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f }, + { 1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f }, + { -1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f }, + // Right face + { 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }, + { 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f }, + // Left Face + { -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f }, + { -1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f }, + { -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f } +}; + +static const uint16_t indices[] = +{ + 0, 1, 2, 2, 3, 0, // Front + 4, 5, 6, 6, 7, 4, // Back + 8, 9, 10, 10, 11, 8, // Top + 12, 13, 14, 14, 15, 12, // Bottom + 16, 17, 18, 18, 19, 16, // Right + 20, 21, 22, 22, 23, 20 // Left +}; + + +static SDL_GPUGraphicsPipeline* psoUnlit = NULL, * psoLight = NULL; +static SDL_GPUGraphicsPipeline* psoBlendUnlit = NULL, * psoBlendLight = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; +static SDL_GPUSampler* samplers[3] = { NULL, NULL, NULL }; +static SDL_GPUTexture* texture = NULL; + +static float projection[16]; + +static bool lighting = false; +static bool blending = false; +struct Light { float ambient[4], diffuse[4], position[4]; } static light = +{ + .ambient = { 0.5f, 0.5f, 0.5f, 1.0f }, + .diffuse = { 1.0f, 1.0f, 1.0f, 1.0f }, + .position = { 0.0f, 0.0f, 2.0f, 1.0f } +}; + +static int filter = 0; + +static float xRot = 0.0f, yRot = 0.0f; +static float xSpeed = 0.0f, ySpeed = 0.0f; +static float z = -5.0f; + + +static bool Lesson8_Init(NeHeContext* restrict ctx) +{ + SDL_GPUShader* vertexShaderUnlit, * fragmentShaderUnlit; + SDL_GPUShader* vertexShaderLight, * fragmentShaderLight; + if (!NeHe_LoadShaders(ctx, &vertexShaderUnlit, &fragmentShaderUnlit, "lesson6", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1, .fragmentSamplers = 1 })) + { + return false; + } + if (!NeHe_LoadShaders(ctx, &vertexShaderLight, &fragmentShaderLight, "lesson7", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 2, .fragmentSamplers = 1 })) + { + SDL_ReleaseGPUShader(ctx->device, fragmentShaderUnlit); + SDL_ReleaseGPUShader(ctx->device, vertexShaderUnlit); + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + .offset = offsetof(Vertex, u) + }, + { + .location = 2, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, nx) + } + }; + + SDL_GPUGraphicsPipelineCreateInfo psoInfo; + SDL_zero(psoInfo); + + psoInfo.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + psoInfo.vertex_input_state = (SDL_GPUVertexInputState) + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }; + + psoInfo.rasterizer_state = (SDL_GPURasterizerState) + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }; + + psoInfo.target_info.num_color_targets = 1; + psoInfo.target_info.depth_stencil_format = appConfig.createDepthFormat; + psoInfo.target_info.has_depth_stencil_target = true; + + // Common pipeline depth & colour target options + psoInfo.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; + const SDL_GPUTextureFormat swapchainTextureFormat = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window); + + // Setup depth/stencil & colour pipeline state for no blending + psoInfo.depth_stencil_state.enable_depth_test = true; + psoInfo.depth_stencil_state.enable_depth_write = true; + psoInfo.target_info.color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = swapchainTextureFormat + }; + + // Create unlit pipeline + psoInfo.vertex_shader = vertexShaderUnlit; + psoInfo.fragment_shader = fragmentShaderUnlit; + psoUnlit = SDL_CreateGPUGraphicsPipeline(ctx->device, &psoInfo); + + // Create lit pipeline + psoInfo.vertex_shader = vertexShaderLight; + psoInfo.fragment_shader = fragmentShaderLight; + psoLight = SDL_CreateGPUGraphicsPipeline(ctx->device, &psoInfo); + + // Setup depth/stencil & colour pipeline state for blending + psoInfo.depth_stencil_state.enable_depth_test = false; + psoInfo.depth_stencil_state.enable_depth_write = false; + psoInfo.target_info.color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = swapchainTextureFormat, + .blend_state = + { + .enable_blend = true, + .color_blend_op = SDL_GPU_BLENDOP_ADD, + .alpha_blend_op = SDL_GPU_BLENDOP_ADD, + .src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA, + .dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE, + .src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA, + .dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE + } + }; + + // Create unlit blended pipeline + psoInfo.vertex_shader = vertexShaderUnlit; + psoInfo.fragment_shader = fragmentShaderUnlit; + psoBlendUnlit = SDL_CreateGPUGraphicsPipeline(ctx->device, &psoInfo); + + // Create lit blended pipeline + psoInfo.vertex_shader = vertexShaderLight; + psoInfo.fragment_shader = fragmentShaderLight; + psoBlendLight = SDL_CreateGPUGraphicsPipeline(ctx->device, &psoInfo); + + // Free shaders + SDL_ReleaseGPUShader(ctx->device, fragmentShaderLight); + SDL_ReleaseGPUShader(ctx->device, vertexShaderLight); + SDL_ReleaseGPUShader(ctx->device, fragmentShaderUnlit); + SDL_ReleaseGPUShader(ctx->device, vertexShaderUnlit); + + if (!psoUnlit || !psoLight || !psoBlendUnlit || !psoBlendLight) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if ((texture = NeHe_LoadTexture(ctx, "Data/Glass.bmp", true, true)) == NULL) + { + return false; + } + + samplers[0] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_NEAREST, + .mag_filter = SDL_GPU_FILTER_NEAREST + }); + samplers[1] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR + }); + samplers[2] = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .min_filter = SDL_GPU_FILTER_LINEAR, + .mag_filter = SDL_GPU_FILTER_LINEAR, + .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, + .max_lod = FLT_MAX + }); + if (!samplers[0] || !samplers[1] || !samplers[2]) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUSampler: %s", SDL_GetError()); + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + return true; +} + +static void Lesson8_Quit(NeHeContext* restrict ctx) +{ + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + for (int i = SDL_arraysize(samplers) - 1; i > 0; --i) + { + SDL_ReleaseGPUSampler(ctx->device, samplers[i]); + } + SDL_ReleaseGPUTexture(ctx->device, texture); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoBlendLight); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoBlendUnlit); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoLight); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, psoUnlit); +} + +static void Lesson8_Resize(NeHeContext* restrict ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson8_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + const SDL_GPUDepthStencilTargetInfo depthInfo = + { + .texture = ctx->depthTexture, + .clear_depth = 1.0f, // Ensure depth buffer clears to furthest value + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_DONT_CARE, + .stencil_load_op = SDL_GPU_LOADOP_DONT_CARE, + .stencil_store_op = SDL_GPU_STOREOP_DONT_CARE, + .cycle = true + }; + + // Begin pass & bind pipeline state + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_GPUGraphicsPipeline* const pipelines[] = { psoUnlit, psoLight, psoBlendUnlit, psoBlendLight }; + SDL_BindGPUGraphicsPipeline(pass, pipelines[lighting + blending * 2]); + + // Bind texture + SDL_BindGPUFragmentSamplers(pass, 0, &(const SDL_GPUTextureSamplerBinding) + { + .texture = texture, + .sampler = samplers[filter] + }, 1); + + // Bind vertex & index buffers + SDL_BindGPUVertexBuffers(pass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUIndexBuffer(pass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + // Setup the view + float model[16]; + Mtx_Translation(model, 0.0f, 0.0f, z); + Mtx_Rotate(model, xRot, 1.0f, 0.0f, 0.0f); + Mtx_Rotate(model, yRot, 0.0f, 1.0f, 0.0f); + + // Push shader uniforms + if (lighting) + { + struct { float model[16], projection[16]; } u; + SDL_memcpy(u.model, model, sizeof(u.model)); + SDL_memcpy(u.projection, projection, sizeof(u.projection)); + SDL_PushGPUVertexUniformData(cmd, 0, &u, sizeof(u)); + SDL_PushGPUVertexUniformData(cmd, 1, &light, sizeof(light)); + } + else + { + struct { float modelViewProj[16], color[4]; } u; + Mtx_Multiply(u.modelViewProj, projection, model); + // 50% translucency + SDL_memcpy(u.color, (const float[4]){ 1.0f, 1.0f, 1.0f, 0.5f }, sizeof(float) * 4); + SDL_PushGPUVertexUniformData(cmd, 0, &u, sizeof(u)); + } + + // Draw textured cube + SDL_DrawGPUIndexedPrimitives(pass, SDL_arraysize(indices), 1, 0, 0, 0); + + SDL_EndGPURenderPass(pass); + + const bool* keys = SDL_GetKeyboardState(NULL); + + if (keys[SDL_SCANCODE_UP]) { xSpeed -= 0.01f; } + if (keys[SDL_SCANCODE_DOWN]) { xSpeed += 0.01f; } + if (keys[SDL_SCANCODE_RIGHT]) { ySpeed += 0.1f; } + if (keys[SDL_SCANCODE_LEFT]) { ySpeed -= 0.1f; } + if (keys[SDL_SCANCODE_PAGEUP]) { z -= 0.02f; } + if (keys[SDL_SCANCODE_PAGEDOWN]) { z += 0.02f; } + + xRot += xSpeed; + yRot += ySpeed; +} + +static void Lesson8_Key(NeHeContext* ctx, SDL_Keycode key, bool down, bool repeat) +{ + (void)ctx; + + if (down && !repeat) + { + switch (key) + { + case SDLK_L: + lighting = !lighting; + break; + case SDLK_B: + blending = !blending; + break; + case SDLK_F: + filter = (filter + 1) % (int)SDL_arraysize(samplers); + break; + default: + break; + } + } +} + + +const struct AppConfig appConfig = +{ + .title = "Tom Stanis & NeHe's Blending Tutorial", + .width = 640, .height = 480, + .createDepthFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM, + .init = Lesson8_Init, + .quit = Lesson8_Quit, + .resize = Lesson8_Resize, + .draw = Lesson8_Draw, + .key = Lesson8_Key +}; diff --git a/src/c/lesson9.c b/src/c/lesson9.c new file mode 100644 index 0000000..4030554 --- /dev/null +++ b/src/c/lesson9.c @@ -0,0 +1,359 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +typedef struct +{ + float x, y, z; + float u, v; +} Vertex; + +static const Vertex vertices[] = +{ + { -1.0f, -1.0f, 0.0f, 0.0f, 0.0f }, + { 1.0f, -1.0f, 0.0f, 1.0f, 0.0f }, + { 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }, + { -1.0f, 1.0f, 0.0f, 0.0f, 1.0f } +}; + +static const uint16_t indices[] = +{ + 0, 1, 2, + 2, 3, 0 +}; + +static SDL_GPUGraphicsPipeline* pso = NULL; +static SDL_GPUBuffer* vtxBuffer = NULL; +static SDL_GPUBuffer* idxBuffer = NULL; +static SDL_GPUTexture* texture = NULL; +static SDL_GPUSampler* sampler = NULL; + +static SDL_GPUBuffer* instanceBuffer = NULL; +static SDL_GPUTransferBuffer* instanceXferBuffer = NULL; + +typedef struct +{ + float model[16]; + float color[4]; +} Instance; + +static float projection[16]; + +static bool twinkle = false; + +static struct Star +{ + float distance, angle; + uint8_t r, g, b; +} stars[50]; + +static float zoom = -15.0f; +static float tilt = 90.0f; +static float spin = 0.0f; + +static uint32_t rngState = 1; +static inline int RngNext(void) +{ + //TODO: check which one of these matches win32 +#if 0 + rngState = rngState * 1103515245 + 12345; +#else + rngState = rngState * 214013 + 2531011; +#endif + return (int)((rngState >> 16) & 0x7FFF); // (s / 65536) % 32768 +} + + +static bool Lesson9_Init(NeHeContext* ctx) +{ + SDL_GPUShader* vertexShader, * fragmentShader; + if (!NeHe_LoadShaders(ctx, &vertexShader, &fragmentShader, "lesson9", + &(const NeHeShaderProgramCreateInfo){ .vertexUniforms = 1, .fragmentSamplers = 1, .vertexStorage = 1 })) + { + return false; + } + + const SDL_GPUVertexAttribute vertexAttribs[] = + { + { + .location = 0, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + .offset = offsetof(Vertex, x) + }, + { + .location = 1, + .buffer_slot = 0, + .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + .offset = offsetof(Vertex, u) + } + }; + pso = SDL_CreateGPUGraphicsPipeline(ctx->device, &(const SDL_GPUGraphicsPipelineCreateInfo) + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .vertex_input_state = + { + .vertex_buffer_descriptions = &(const SDL_GPUVertexBufferDescription) + { + .slot = 0, + .pitch = sizeof(Vertex), + .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX + }, + .num_vertex_buffers = 1, + .vertex_attributes = vertexAttribs, + .num_vertex_attributes = SDL_arraysize(vertexAttribs) + }, + .rasterizer_state = + { + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + }, + .target_info = + { + .color_target_descriptions = &(const SDL_GPUColorTargetDescription) + { + .format = SDL_GetGPUSwapchainTextureFormat(ctx->device, ctx->window), + .blend_state = + { + .enable_blend = true, + .color_blend_op = SDL_GPU_BLENDOP_ADD, + .alpha_blend_op = SDL_GPU_BLENDOP_ADD, + .src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA, + .dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE, + .src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA, + .dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE + } + }, + .num_color_targets = 1 + } + }); + SDL_ReleaseGPUShader(ctx->device, fragmentShader); + SDL_ReleaseGPUShader(ctx->device, vertexShader); + if (!pso) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUGraphicsPipeline: %s", SDL_GetError()); + return false; + } + + if ((texture = NeHe_LoadTexture(ctx, "Data/Star.bmp", true, false)) == NULL) + { + return false; + } + + sampler = SDL_CreateGPUSampler(ctx->device, &(const SDL_GPUSamplerCreateInfo) + { + .mag_filter = SDL_GPU_FILTER_LINEAR, + .min_filter = SDL_GPU_FILTER_LINEAR + }); + if (!sampler) + { + return false; + } + + if (!NeHe_CreateVertexIndexBuffer(ctx, &vtxBuffer, &idxBuffer, + vertices, sizeof(vertices), + indices, sizeof(indices))) + { + return false; + } + + const int numStars = SDL_arraysize(stars); + + instanceBuffer = SDL_CreateGPUBuffer(ctx->device, &(const SDL_GPUBufferCreateInfo) + { + .usage = SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, + .size = sizeof(Instance) * 2 * numStars + }); + if (!instanceBuffer) + { + return false; + } + instanceXferBuffer = SDL_CreateGPUTransferBuffer(ctx->device, &(const SDL_GPUTransferBufferCreateInfo) + { + .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + .size = sizeof(Instance) * numStars + }); + if (!instanceXferBuffer) + { + return false; + } + + // Initialise stars + for (int i = 0; i < numStars; ++i) + { + stars[i] = (struct Star) + { + .angle = 0.0f, + .distance = 5.0f * ((float)i / (float)numStars), + .r = (uint8_t)(RngNext() % 256), + .g = (uint8_t)(RngNext() % 256), + .b = (uint8_t)(RngNext() % 256) + }; + } + + return true; +} + +static void Lesson9_Quit(NeHeContext* ctx) +{ + SDL_ReleaseGPUTransferBuffer(ctx->device, instanceXferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, instanceBuffer); + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + SDL_ReleaseGPUSampler(ctx->device, sampler); + SDL_ReleaseGPUTexture(ctx->device, texture); + SDL_ReleaseGPUGraphicsPipeline(ctx->device, pso); +} + +static void Lesson9_Resize(NeHeContext* ctx, int width, int height) +{ + (void)ctx; + + // Avoid division by zero by clamping height + height = SDL_max(height, 1); + // Recalculate projection matrix + Mtx_Perspective(projection, 45.0f, (float)width / (float)height, 0.1f, 100.0f); +} + +static void Lesson9_Draw(NeHeContext* restrict ctx, SDL_GPUCommandBuffer* restrict cmd, SDL_GPUTexture* restrict swapchain) +{ + const SDL_GPUColorTargetInfo colorInfo = + { + .texture = swapchain, + .clear_color = { 0.0f, 0.0f, 0.0f, 0.5f }, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE + }; + + static const int numStars = SDL_arraysize(stars); + + // Animate stars + Instance* instances = SDL_MapGPUTransferBuffer(ctx->device, instanceXferBuffer, true); + for (int i = 0, instanceIdx = 0; i < numStars; ++i, ++instanceIdx) + { + struct Star* star = &stars[i]; + Instance* instance = &instances[instanceIdx]; + + Mtx_Translation(instance->model, 0.0f ,0.0f, zoom); + Mtx_Rotate(instance->model, tilt, 1.0f, 0.0f, 0.0f); + Mtx_Rotate(instance->model, star->angle, 0.0f, 1.0f, 0.0f); + Mtx_Translate(instance->model, star->distance, 0.0f, 0.0f); + Mtx_Rotate(instance->model, -star->angle, 0.0f, 1.0f, 0.0f); + Mtx_Rotate(instance->model, -tilt, 1.0f, 0.0f, 0.0f); + + if (twinkle) + { + instance->color[0] = (float)stars[numStars - i - 1].r / 255.0f; + instance->color[1] = (float)stars[numStars - i - 1].g / 255.0f; + instance->color[2] = (float)stars[numStars - i - 1].b / 255.0f; + instance->color[3] = 1.0f; + SDL_memcpy(instances[++instanceIdx].model, instance->model, sizeof(float) * 16); + instance = &instances[instanceIdx]; + } + + Mtx_Rotate(instance->model, spin, 0.0f, 0.0f, 1.0f); + instance->color[0] = (float)star->r / 255.0f; + instance->color[1] = (float)star->g / 255.0f; + instance->color[2] = (float)star->b / 255.0f; + instance->color[3] = 1.0f; + + spin += 0.01f; + star->angle += (float)i / (float)numStars; + star->distance -= 0.01f; + if (star->distance < 0.0f) + { + star->distance += 5.0f; + star->r = (uint8_t)(RngNext() % 256); + star->g = (uint8_t)(RngNext() % 256); + star->b = (uint8_t)(RngNext() % 256); + } + } + SDL_UnmapGPUTransferBuffer(ctx->device, instanceXferBuffer); + + const unsigned numInstances = twinkle ? 2 * (unsigned)numStars : (unsigned)numStars; + + // Upload instances buffer to the GPU + SDL_GPUCopyPass* copyPass = SDL_BeginGPUCopyPass(cmd); + SDL_UploadToGPUBuffer(copyPass, &(const SDL_GPUTransferBufferLocation) + { + .transfer_buffer = instanceXferBuffer, + .offset = 0 + }, &(const SDL_GPUBufferRegion) + { + .buffer = instanceBuffer, + .offset = 0, + .size = sizeof(Instance) * numInstances + }, true); + SDL_EndGPUCopyPass(copyPass); + + // Begin pass & bind pipeline state + SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, NULL); + SDL_BindGPUGraphicsPipeline(renderPass, pso); + + // Bind particle texture + SDL_BindGPUFragmentSamplers(renderPass, 0, &(const SDL_GPUTextureSamplerBinding) + { + .texture = texture, + .sampler = sampler + }, 1); + + SDL_BindGPUVertexBuffers(renderPass, 0, &(const SDL_GPUBufferBinding) + { + .buffer = vtxBuffer, + .offset = 0 + }, 1); + SDL_BindGPUVertexStorageBuffers(renderPass, 0, &instanceBuffer, 1); + SDL_BindGPUIndexBuffer(renderPass, &(const SDL_GPUBufferBinding) + { + .buffer = idxBuffer, + .offset = 0 + }, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + SDL_PushGPUVertexUniformData(cmd, 0, projection, sizeof(projection)); + SDL_DrawGPUIndexedPrimitives(renderPass, SDL_arraysize(indices), numInstances, 0, 0, 0); + + SDL_EndGPURenderPass(renderPass); + + const bool* keys = SDL_GetKeyboardState(NULL); + + if (keys[SDL_SCANCODE_UP]) { tilt -= 0.5f; } + if (keys[SDL_SCANCODE_DOWN]) { tilt += 0.5f; } + if (keys[SDL_SCANCODE_PAGEUP]) { zoom -= 0.2f; } + if (keys[SDL_SCANCODE_PAGEDOWN]) { zoom += 0.2f; } +} + +static void Lesson9_Key(NeHeContext* ctx, SDL_Keycode key, bool down, bool repeat) +{ + (void)ctx; + + if (down && !repeat) + { + switch (key) + { + case SDLK_T: + twinkle = !twinkle; + break; + default: + break; + } + } +} + + +const struct AppConfig appConfig = +{ + .title = "NeHe's Animated Blended Textures Tutorial", + .width = 640, .height = 480, + .init = Lesson9_Init, + .quit = Lesson9_Quit, + .resize = Lesson9_Resize, + .draw = Lesson9_Draw, + .key = Lesson9_Key +}; diff --git a/src/c/matrix.c b/src/c/matrix.c new file mode 100644 index 0000000..8cfb9aa --- /dev/null +++ b/src/c/matrix.c @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "matrix.h" + + +extern inline void Mtx_Identity(float m[16]); +extern inline void Mtx_Translation(float m[16], float x, float y, float z); + +static void MakeRotation(float m[9], float c, float s, float x, float y, float z) +{ + const float rc = 1.f - c; + const float rcx = x * rc, rcy = y * rc, rcz = z * rc; + const float sx = x * s, sy = y * s, sz = z * s; + + m[0] = rcx * x + c; + m[3] = rcx * y - sz; + m[6] = rcx * z + sy; + + m[1] = rcy * x + sz; + m[4] = rcy * y + c; + m[7] = rcy * z - sx; + + m[2] = rcz * x - sy; + m[5] = rcz * y + sx; + m[8] = rcz * z + c; +} + +static void MakeGLRotation(float m[9], float angle, float x, float y, float z) +{ + // Treat inputs like glRotatef + const float theta = angle * (SDL_PI_F / 180.0f); + const float axisMag = SDL_sqrtf(x * x + y * y + z * z); + if (SDL_fabsf(axisMag - 1.f) > SDL_FLT_EPSILON) + { + x /= axisMag; + y /= axisMag; + z /= axisMag; + } + + MakeRotation(m, SDL_cosf(theta), SDL_sinf(theta), x, y, z); +} + +void Mtx_Rotation(float m[16], float angle, float x, float y, float z) +{ + float r[9]; + MakeGLRotation(r, angle, x, y, z); + + m[3] = m[7] = m[11] = m[12] = m[13] = m[14] = 0.0f; + m[15] = 1.0f; + + m[0] = r[0]; m[1] = r[1]; m[2] = r[2]; + m[4] = r[3]; m[5] = r[4]; m[6] = r[5]; + m[8] = r[6]; m[9] = r[7]; m[10] = r[8]; +} + +void Mtx_Perspective(float m[16], float fovy, float aspect, float near, float far) +{ + const float h = 1.0f / SDL_tanf(fovy * (SDL_PI_F / 180.0f) * 0.5f); + const float w = h / aspect; + const float invClipRng = 1.0f / (far - near); + const float zh = -(far + near) * invClipRng; + const float zl = -(2.0f * far * near) * invClipRng; + + /* + [w 0 0 0] + [0 h 0 0] + [0 0 zh zl] + [0 0 -1 0] + */ + m[1] = m[2] = m[3] = m[4] = m[6] = m[7] = m[8] = m[9] = m[12] = m[13] = m[15] = 0.0f; + m[0] = w; + m[5] = h; + m[10] = zh; + m[14] = zl; + m[11] = -1.0f; +} + +void Mtx_Multiply(float m[16], const float l[16], const float r[16]) +{ + int i = 0; + for (int col = 0; col < 4; ++col) + { + for (int row = 0; row < 4; ++row) + { + float a = 0.f; + for (int j = 0; j < 4; ++j) + { + a += l[j * 4 + row] * r[col * 4 + j]; + } + m[i++] = a; + } + } +} + +void Mtx_Translate(float m[16], float x, float y, float z) +{ + /* + m = { [1 0 0 x] + [0 1 0 y] + [0 0 1 z] + [0 0 0 1] } * m + */ + m[12] += x * m[0] + y * m[4] + z * m[8]; + m[13] += x * m[1] + y * m[5] + z * m[9]; + m[14] += x * m[2] + y * m[6] + z * m[10]; + m[15] += x * m[3] + y * m[7] + z * m[11]; +} + +void Mtx_Rotate(float m[16], float angle, float x, float y, float z) +{ + // Set up temporaries + float tmp[12], r[9]; + SDL_memcpy(tmp, m, sizeof(float) * 12); + MakeGLRotation(r, angle, x, y, z); + + // Partial matrix multiplication + m[0] = r[0] * tmp[0] + r[1] * tmp[4] + r[2] * tmp[8]; + m[1] = r[0] * tmp[1] + r[1] * tmp[5] + r[2] * tmp[9]; + m[2] = r[0] * tmp[2] + r[1] * tmp[6] + r[2] * tmp[10]; + m[3] = r[0] * tmp[3] + r[1] * tmp[7] + r[2] * tmp[11]; + m[4] = r[3] * tmp[0] + r[4] * tmp[4] + r[5] * tmp[8]; + m[5] = r[3] * tmp[1] + r[4] * tmp[5] + r[5] * tmp[9]; + m[6] = r[3] * tmp[2] + r[4] * tmp[6] + r[5] * tmp[10]; + m[7] = r[3] * tmp[3] + r[4] * tmp[7] + r[5] * tmp[11]; + m[8] = r[6] * tmp[0] + r[7] * tmp[4] + r[8] * tmp[8]; + m[9] = r[6] * tmp[1] + r[7] * tmp[5] + r[8] * tmp[9]; + m[10] = r[6] * tmp[2] + r[7] * tmp[6] + r[8] * tmp[10]; + m[11] = r[6] * tmp[3] + r[7] * tmp[7] + r[8] * tmp[11]; +} diff --git a/src/c/matrix.h b/src/c/matrix.h new file mode 100644 index 0000000..71b70c7 --- /dev/null +++ b/src/c/matrix.h @@ -0,0 +1,36 @@ +#ifndef MATRIX_H +#define MATRIX_H + +#include + +inline void Mtx_Identity(float m[16]) +{ + SDL_memcpy(m, (float[]) + { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }, sizeof(float) * 16); +} + +inline void Mtx_Translation(float m[16], float x, float y, float z) +{ + SDL_memcpy(m, (float[]) + { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + x, y, z, 1 + }, sizeof(float) * 16); +} + +void Mtx_Rotation(float m[16], float angle, float x, float y, float z); +void Mtx_Perspective(float m[16], float fovy, float aspect, float near, float far); + +void Mtx_Multiply(float m[16], const float l[16], const float r[16]); + +void Mtx_Translate(float m[16], float x, float y, float z); +void Mtx_Rotate(float m[16], float angle, float x, float y, float z); + +#endif//MATRIX_H diff --git a/src/c/nehe.c b/src/c/nehe.c new file mode 100644 index 0000000..620860f --- /dev/null +++ b/src/c/nehe.c @@ -0,0 +1,649 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include "nehe.h" + + +bool NeHe_InitGPU(NeHeContext* ctx, const char* title, int width, int height) +{ + // Create window + ctx->window = SDL_CreateWindow(title, width, height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); + if (!ctx->window) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow: %s", SDL_GetError()); + return false; + } + + // Open GPU device + const SDL_GPUShaderFormat formats = + SDL_GPU_SHADERFORMAT_METALLIB | SDL_GPU_SHADERFORMAT_MSL | + SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL; + ctx->device = SDL_CreateGPUDevice(formats, true, NULL); + if (!ctx->device) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUDevice: %s", SDL_GetError()); + return false; + } + + // Attach window to the GPU device + if (!SDL_ClaimWindowForGPUDevice(ctx->device, ctx->window)) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_ClaimWindowForGPUDevice: %s", SDL_GetError()); + return false; + } + + // Enable VSync + SDL_SetGPUSwapchainParameters(ctx->device, ctx->window, + SDL_GPU_SWAPCHAINCOMPOSITION_SDR, + SDL_GPU_PRESENTMODE_VSYNC); + + return true; +} + +bool NeHe_SetupDepthTexture(NeHeContext* ctx, uint32_t width, uint32_t height, + SDL_GPUTextureFormat format, float clearDepth) +{ + if (ctx->depthTexture) + { + SDL_ReleaseGPUTexture(ctx->device, ctx->depthTexture); + ctx->depthTexture = NULL; + } + + SDL_PropertiesID props = SDL_CreateProperties(); + if (props == 0) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateProperties: %s", SDL_GetError()); + return false; + } + // Workaround for https://github.com/libsdl-org/SDL/issues/10758 + SDL_SetFloatProperty(props, SDL_PROP_GPU_TEXTURE_CREATE_D3D12_CLEAR_DEPTH_FLOAT, clearDepth); + + SDL_GPUTexture* texture = SDL_CreateGPUTexture(ctx->device, &(const SDL_GPUTextureCreateInfo) + { + .type = SDL_GPU_TEXTURETYPE_2D, + .format = format, + .width = width, + .height = height, + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = SDL_GPU_SAMPLECOUNT_1, + .usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, + .props = props + }); + SDL_DestroyProperties(props); + if (!texture) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUTexture: %s", SDL_GetError()); + return false; + } + + SDL_SetGPUTextureName(ctx->device, texture, "Depth Buffer Texture"); + ctx->depthTexture = texture; + ctx->depthTextureWidth = width; + ctx->depthTextureHeight = height; + return true; +} + +char* NeHe_ResourcePath(const NeHeContext* restrict ctx, const char* const restrict resourcePath) +{ + SDL_assert(ctx && ctx->baseDir && resourcePath); + + // Build path to resouce: "{baseDir}/{resourcePath}" + const size_t baseLen = SDL_strlen(ctx->baseDir); + const size_t resourcePathLen = SDL_strlen(resourcePath); + char* path = SDL_malloc(baseLen + resourcePathLen + 1); + if (!path) + { + return NULL; + } + SDL_memcpy(path, ctx->baseDir, baseLen); + SDL_memcpy(&path[baseLen], resourcePath, resourcePathLen); + path[baseLen + resourcePathLen] = '\0'; + return path; +} + +extern inline SDL_IOStream* NeHe_OpenResource(const NeHeContext* restrict ctx, + const char* restrict resourcePath, + const char* restrict mode); + +SDL_GPUTexture* NeHe_LoadTexture(NeHeContext* restrict ctx, const char* const restrict resourcePath, + bool flipVertical, bool genMipmaps) +{ + char* path = NeHe_ResourcePath(ctx, resourcePath); + if (!path) + { + return NULL; + } + + // Load image into a surface + SDL_Surface* image = SDL_LoadBMP(path); + SDL_free(path); + if (!image) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_LoadBMP: %s", SDL_GetError()); + return NULL; + } + + // Flip surface if requested + if (flipVertical && !SDL_FlipSurface(image, SDL_FLIP_VERTICAL)) + { + SDL_DestroySurface(image); + return NULL; + } + + // Upload texture to GPU + SDL_GPUTexture* texture = NeHe_CreateGPUTextureFromSurface(ctx, image, genMipmaps); + SDL_DestroySurface(image); + if (!texture) + { + return NULL; + } + + return texture; +} + +static SDL_GPUTexture* CreateTextureFromPixels(SDL_GPUDevice* restrict device, + const void* restrict data, size_t dataSize, const SDL_GPUTextureCreateInfo* restrict createInfo, bool genMipmaps) +{ + SDL_assert(dataSize <= UINT32_MAX); + + SDL_GPUTexture* texture = SDL_CreateGPUTexture(device, createInfo); + if (!texture) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUTexture: %s", SDL_GetError()); + return NULL; + } + + // Create and copy image data to a transfer buffer + SDL_GPUTransferBuffer* xferBuffer = SDL_CreateGPUTransferBuffer(device, &(const SDL_GPUTransferBufferCreateInfo) + { + .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + .size = (Uint32)dataSize + }); + if (!xferBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTexture(device, texture); + return NULL; + } + + void* map = SDL_MapGPUTransferBuffer(device, xferBuffer, false); + if (!map) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_MapGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(device, xferBuffer); + SDL_ReleaseGPUTexture(device, texture); + return NULL; + } + SDL_memcpy(map, data, dataSize); + SDL_UnmapGPUTransferBuffer(device, xferBuffer); + + // Upload the transfer data to the GPU resources + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device); + if (!cmd) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AcquireGPUCommandBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(device, xferBuffer); + SDL_ReleaseGPUTexture(device, texture); + return NULL; + } + + SDL_GPUCopyPass* pass = SDL_BeginGPUCopyPass(cmd); + SDL_UploadToGPUTexture(pass, &(const SDL_GPUTextureTransferInfo) + { + .transfer_buffer = xferBuffer, + .offset = 0 + }, &(const SDL_GPUTextureRegion) + { + .texture = texture, + .w = createInfo->width, + .h = createInfo->height, + .d = createInfo->layer_count_or_depth + }, false); + SDL_EndGPUCopyPass(pass); + + if (genMipmaps) + { + SDL_GenerateMipmapsForGPUTexture(cmd, texture); + } + + SDL_SubmitGPUCommandBuffer(cmd); + SDL_ReleaseGPUTransferBuffer(device, xferBuffer); + return texture; +} + +SDL_GPUTexture* NeHe_CreateGPUTextureFromSurface(NeHeContext* restrict ctx, const SDL_Surface* restrict surface, + bool genMipmaps) +{ + SDL_GPUTextureCreateInfo info; + SDL_zero(info); + info.type = SDL_GPU_TEXTURETYPE_2D; + info.format = SDL_GPU_TEXTUREFORMAT_INVALID; + info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; + info.width = (Uint32)surface->w; + info.height = (Uint32)surface->h; + info.layer_count_or_depth = 1; + info.num_levels = 1; + + bool needsConvert = false; + switch (surface->format) + { + // FIMXE: I'm not sure that these are endian-safe + case SDL_PIXELFORMAT_RGBA32: info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; break; + case SDL_PIXELFORMAT_RGBA64: info.format = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UNORM; break; + case SDL_PIXELFORMAT_RGB565: info.format = SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM; break; + case SDL_PIXELFORMAT_ARGB1555: info.format = SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM; break; + case SDL_PIXELFORMAT_BGRA4444: info.format = SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM; break; + case SDL_PIXELFORMAT_BGRA32: info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; break; + case SDL_PIXELFORMAT_RGBA64_FLOAT: info.format = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT; break; + case SDL_PIXELFORMAT_RGBA128_FLOAT: info.format = SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT; break; + default: + needsConvert = true; + info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; + break; + } + + size_t dataSize = (size_t)surface->w * (size_t)surface->h; + void* data = NULL; + SDL_Surface* conv = NULL; + if (needsConvert) + { + // Convert pixel format if required + if ((conv = SDL_ConvertSurface((SDL_Surface*)surface, SDL_PIXELFORMAT_ABGR8888)) == NULL) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_ConvertSurface: %s", SDL_GetError()); + SDL_free(data); + return NULL; + } + dataSize *= SDL_BYTESPERPIXEL(conv->format); + data = conv->pixels; + } + else + { + dataSize *= SDL_BYTESPERPIXEL(surface->format); + data = surface->pixels; + } + + if (genMipmaps) + { + info.usage |= SDL_GPU_TEXTUREUSAGE_COLOR_TARGET; + // floor(log₂(max(𝑤,ℎ)) + 1 + info.num_levels = (Uint32)SDL_MostSignificantBitIndex32(SDL_max(info.width, info.height)) + 1; + } + + SDL_GPUTexture* texture = CreateTextureFromPixels(ctx->device, data, dataSize, &info, genMipmaps); + SDL_DestroySurface(conv); + return texture; +} + +static char* ReadBlob(const char* const restrict path, size_t* restrict outLength) +{ + SDL_ClearError(); + SDL_IOStream* file = SDL_IOFromFile(path, "rb"); + if (!file) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_IOFromFile: %s", SDL_GetError()); + return NULL; + } + + // Allocate a buffer of the size of the file + if (SDL_SeekIO(file, 0, SDL_IO_SEEK_END) == -1) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_SeekIO: %s", SDL_GetError()); + } + const int64_t size = SDL_TellIO(file); + char* data; + if (size < 0 || (data = SDL_malloc((size_t)size)) == NULL) + { + SDL_CloseIO(file); + return NULL; + } + if (SDL_SeekIO(file, 0, SDL_IO_SEEK_SET) != 0) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_SeekIO: %s", SDL_GetError()); + } + + SDL_ClearError(); + // Read the file contents into the buffer + const size_t read = SDL_ReadIO(file, data, (size_t)size); + if (read == 0 && SDL_GetIOStatus(file) == SDL_IO_STATUS_ERROR) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_ReadIO: %s", SDL_GetError()); + } + if (!SDL_CloseIO(file)) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CloseIO: %s", SDL_GetError()); + } + if (read != (size_t)size) + { + SDL_free(data); + return NULL; + } + + *outLength = (size_t)size; + return data; +} + +static SDL_GPUShader* LoadShaderBlob(NeHeContext* restrict ctx, + const char* restrict code, size_t codeLen, + const NeHeShaderProgramCreateInfo* restrict info, + SDL_GPUShaderFormat format, SDL_GPUShaderStage type, + const char* const restrict main) +{ + if (!code) + { + return NULL; + } + + SDL_GPUShader* shader = SDL_CreateGPUShader(ctx->device, &(const SDL_GPUShaderCreateInfo) + { + .code_size = codeLen, + .code = (const Uint8*)code, + .entrypoint = main, + .format = format, + .stage = type, + .num_samplers = (type == SDL_GPU_SHADERSTAGE_FRAGMENT) ? info->fragmentSamplers : 0, + .num_storage_buffers = (type == SDL_GPU_SHADERSTAGE_VERTEX) ? info->vertexUniforms : 0, + .num_uniform_buffers = (type == SDL_GPU_SHADERSTAGE_VERTEX) ? info->vertexUniforms : 0, + }); + if (!shader) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUShader: %s", SDL_GetError()); + return NULL; + } + return shader; +} + +static SDL_GPUShader* LoadShader(NeHeContext* restrict ctx, const char* restrict path, + const NeHeShaderProgramCreateInfo* restrict info, SDL_GPUShaderFormat format, SDL_GPUShaderStage type, + const char* const restrict main) +{ + size_t size; + char* data = ReadBlob(path, &size); + SDL_GPUShader *shader = LoadShaderBlob(ctx, data, size, info, format, type, main); + SDL_free(data); + return shader; +} + +bool NeHe_LoadShaders(NeHeContext* restrict ctx, + SDL_GPUShader** restrict outVertex, + SDL_GPUShader** restrict outFragment, + const char* restrict name, + const NeHeShaderProgramCreateInfo* restrict info) + //unsigned vertexUniforms, unsigned fragmentSamplers, unsigned vertexStorage) +{ + SDL_GPUShader *vtxShader = NULL, *frgShader = NULL; + + // Build path to shader: "{base}/Shaders/{name}.{ext}" + const char* resources = SDL_GetBasePath(); // Resources directory + const size_t resourcesLen = SDL_strlen(resources); + const size_t nameLen = SDL_strlen(name); + const size_t basenameLen = resourcesLen + 8 + nameLen; + char* path = SDL_malloc(basenameLen + 10); + if (!path) + { + return false; + } + SDL_memcpy(path, resources, resourcesLen); + SDL_memcpy(&path[resourcesLen], "Shaders", 7); + path[resourcesLen + 7] = resources[resourcesLen - 1]; // Copy path separator + SDL_memcpy(&path[resourcesLen + 8], name, nameLen); + + const SDL_GPUShaderFormat availableFormats = SDL_GetGPUShaderFormats(ctx->device); + if (availableFormats & (SDL_GPU_SHADERFORMAT_METALLIB | SDL_GPU_SHADERFORMAT_MSL)) + { + size_t size; + if (availableFormats & SDL_GPU_SHADERFORMAT_METALLIB) // Apple Metal (compiled library) + { + const SDL_GPUShaderFormat format = SDL_GPU_SHADERFORMAT_METALLIB; + SDL_memcpy(&path[basenameLen], ".metallib", 10); + char* lib = ReadBlob(path, &size); + vtxShader = LoadShaderBlob(ctx, lib, size, info, format, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain"); + frgShader = LoadShaderBlob(ctx, lib, size, info, format, SDL_GPU_SHADERSTAGE_FRAGMENT, "FragmentMain"); + SDL_free(lib); + } + if ((!vtxShader || !frgShader) && availableFormats & SDL_GPU_SHADERFORMAT_MSL) // Apple Metal (source) + { + const SDL_GPUShaderFormat format = SDL_GPU_SHADERFORMAT_MSL; + SDL_memcpy(&path[basenameLen], ".metal", 7); + char* src = ReadBlob(path, &size); + if (!vtxShader) + vtxShader = LoadShaderBlob(ctx, src, size, info, format, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain"); + if (!frgShader) + frgShader = LoadShaderBlob(ctx, src, size, info, format, SDL_GPU_SHADERSTAGE_FRAGMENT, "FragmentMain"); + SDL_free(src); + } + } + else if (availableFormats & SDL_GPU_SHADERFORMAT_SPIRV) // Vulkan + { + const SDL_GPUShaderFormat format = SDL_GPU_SHADERFORMAT_SPIRV; + SDL_memcpy(&path[basenameLen], ".vtx.spv", 9); + vtxShader = LoadShader(ctx, path, info, format, SDL_GPU_SHADERSTAGE_VERTEX, "main"); + SDL_memcpy(&path[basenameLen], ".frg.spv", 9); + frgShader = LoadShader(ctx, path, info, format, SDL_GPU_SHADERSTAGE_FRAGMENT, "main"); + } + else if (availableFormats & SDL_GPU_SHADERFORMAT_DXIL) // Direct3D 12 Shader Model 6.0 + { + const SDL_GPUShaderFormat format = SDL_GPU_SHADERFORMAT_DXIL; + SDL_memcpy(&path[basenameLen], ".vtx.dxb", 9); + vtxShader = LoadShader(ctx, path, info, format, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain"); + SDL_memcpy(&path[basenameLen], ".pxl.dxb", 9); + frgShader = LoadShader(ctx, path, info, format, SDL_GPU_SHADERSTAGE_FRAGMENT, "PixelMain"); + } + + SDL_free(path); + if (!vtxShader || !frgShader) + { + if (vtxShader) + SDL_ReleaseGPUShader(ctx->device, vtxShader); + if (frgShader) + SDL_ReleaseGPUShader(ctx->device, frgShader); + return false; + } + + *outVertex = vtxShader; + *outFragment = frgShader; + return true; +} + +SDL_GPUBuffer* NeHe_CreateVertexBuffer(NeHeContext* restrict ctx, const void* restrict vertices, uint32_t verticesSize) +{ + // Create vertex data buffer + SDL_GPUBuffer* buffer = SDL_CreateGPUBuffer(ctx->device, &(const SDL_GPUBufferCreateInfo) + { + .usage = SDL_GPU_BUFFERUSAGE_VERTEX, + .size = verticesSize + }); + if (!buffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUBuffer: %s", SDL_GetError()); + return false; + } + + // Create vertex transfer buffer + SDL_GPUTransferBuffer* xferBuffer = SDL_CreateGPUTransferBuffer(ctx->device, &(const SDL_GPUTransferBufferCreateInfo) + { + .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + .size = verticesSize + }); + if (!xferBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUBuffer(ctx->device, buffer); + return false; + } + + // Map transfer buffer and copy the vertex data + Uint8* map = SDL_MapGPUTransferBuffer(ctx->device, xferBuffer, false); + if (!map) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_MapGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(ctx->device, xferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, buffer); + return false; + } + SDL_memcpy(map, vertices, (size_t)verticesSize); + SDL_UnmapGPUTransferBuffer(ctx->device, xferBuffer); + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(ctx->device); + if (!cmd) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AcquireGPUCommandBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(ctx->device, xferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, buffer); + return false; + } + + // Upload the vertex & index data into the GPU buffer(s) + SDL_GPUCopyPass* pass = SDL_BeginGPUCopyPass(cmd); + SDL_UploadToGPUBuffer(pass, &(const SDL_GPUTransferBufferLocation) + { + .transfer_buffer = xferBuffer, + .offset = 0 + }, &(const SDL_GPUBufferRegion) + { + .buffer = buffer, + .offset = 0, + .size = verticesSize + }, false); + SDL_EndGPUCopyPass(pass); + SDL_SubmitGPUCommandBuffer(cmd); + + SDL_ReleaseGPUTransferBuffer(ctx->device, xferBuffer); + + return buffer; +} + +bool NeHe_CreateVertexIndexBuffer(NeHeContext* restrict ctx, + SDL_GPUBuffer** restrict outVertexBuffer, + SDL_GPUBuffer** restrict outIndexBuffer, + const void* restrict vertices, uint32_t verticesSize, + const void* restrict indices, uint32_t indicesSize) +{ + // Create vertex data buffer + SDL_GPUBuffer* vtxBuffer = SDL_CreateGPUBuffer(ctx->device, &(const SDL_GPUBufferCreateInfo) + { + .usage = SDL_GPU_BUFFERUSAGE_VERTEX, + .size = verticesSize + }); + if (!vtxBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUBuffer: %s", SDL_GetError()); + return false; + } + + // Create index data buffer + SDL_GPUBuffer* idxBuffer = SDL_CreateGPUBuffer(ctx->device, &(const SDL_GPUBufferCreateInfo) + { + .usage = SDL_GPU_BUFFERUSAGE_INDEX, + .size = indicesSize + }); + if (!idxBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + return false; + } + + // Create vertex transfer buffer + SDL_GPUTransferBuffer* vtxXferBuffer = SDL_CreateGPUTransferBuffer(ctx->device, &(const SDL_GPUTransferBufferCreateInfo) + { + .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + .size = verticesSize + }); + if (!vtxXferBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + return false; + } + + // Create index transfer buffer + SDL_GPUTransferBuffer* idxXferBuffer = SDL_CreateGPUTransferBuffer(ctx->device, &(const SDL_GPUTransferBufferCreateInfo) + { + .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + .size = indicesSize + }); + if (!idxXferBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(ctx->device, vtxXferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + return false; + } + + // Map transfer buffer and copy the vertex data + Uint8* map = SDL_MapGPUTransferBuffer(ctx->device, vtxXferBuffer, false); + if (!map) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_MapGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(ctx->device, idxXferBuffer); + SDL_ReleaseGPUTransferBuffer(ctx->device, vtxXferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + return false; + } + SDL_memcpy(map, vertices, (size_t)verticesSize); + SDL_UnmapGPUTransferBuffer(ctx->device, vtxXferBuffer); + + // Map transfer buffer and copy the index data + map = SDL_MapGPUTransferBuffer(ctx->device, idxXferBuffer, false); + if (!map) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_MapGPUTransferBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(ctx->device, idxXferBuffer); + SDL_ReleaseGPUTransferBuffer(ctx->device, vtxXferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + return false; + } + SDL_memcpy(map, indices, (size_t)indicesSize); + SDL_UnmapGPUTransferBuffer(ctx->device, idxXferBuffer); + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(ctx->device); + if (!cmd) + { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AcquireGPUCommandBuffer: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(ctx->device, idxXferBuffer); + SDL_ReleaseGPUTransferBuffer(ctx->device, vtxXferBuffer); + SDL_ReleaseGPUBuffer(ctx->device, idxBuffer); + SDL_ReleaseGPUBuffer(ctx->device, vtxBuffer); + return false; + } + + // Upload the vertex & index data into the GPU buffer(s) + SDL_GPUCopyPass* pass = SDL_BeginGPUCopyPass(cmd); + SDL_UploadToGPUBuffer(pass, &(const SDL_GPUTransferBufferLocation) + { + .transfer_buffer = vtxXferBuffer, + .offset = 0 + }, &(const SDL_GPUBufferRegion) + { + .buffer = vtxBuffer, + .offset = 0, + .size = verticesSize + }, false); + SDL_UploadToGPUBuffer(pass, &(const SDL_GPUTransferBufferLocation) + { + .transfer_buffer = idxXferBuffer, + .offset = 0 + }, &(const SDL_GPUBufferRegion) + { + .buffer = idxBuffer, + .offset = 0, + .size = indicesSize + }, false); + SDL_EndGPUCopyPass(pass); + SDL_SubmitGPUCommandBuffer(cmd); + + SDL_ReleaseGPUTransferBuffer(ctx->device, idxXferBuffer); + SDL_ReleaseGPUTransferBuffer(ctx->device, vtxXferBuffer); + + *outVertexBuffer = vtxBuffer; + *outIndexBuffer = idxBuffer; + return true; +} diff --git a/src/c/nehe.h b/src/c/nehe.h new file mode 100644 index 0000000..2f72ef1 --- /dev/null +++ b/src/c/nehe.h @@ -0,0 +1,62 @@ +#ifndef NEHE_H +#define NEHE_H + +#include "application.h" +#include "matrix.h" + +#include +#include +#include + +typedef struct NeHeContext +{ + SDL_Window* window; + SDL_GPUDevice* device; + SDL_GPUTexture* depthTexture; + uint32_t depthTextureWidth, depthTextureHeight; + + const char* baseDir; + +} NeHeContext; + +typedef struct +{ + unsigned vertexUniforms; + unsigned vertexStorage; + unsigned fragmentSamplers; +} NeHeShaderProgramCreateInfo; + +bool NeHe_InitGPU(NeHeContext* ctx, const char* title, int width, int height); +bool NeHe_SetupDepthTexture(NeHeContext* ctx, uint32_t width, uint32_t height, + SDL_GPUTextureFormat format, float clearDepth); +char* NeHe_ResourcePath(const NeHeContext* restrict ctx, const char* restrict resourcePath); +inline SDL_IOStream* NeHe_OpenResource(const NeHeContext* restrict ctx, + const char* const restrict resourcePath, + const char* const restrict mode) +{ + char* path = NeHe_ResourcePath(ctx, resourcePath); + if (!path) + { + return NULL; + } + SDL_IOStream* file = SDL_IOFromFile(path, mode); + SDL_free(path); + return file; +} +SDL_GPUTexture* NeHe_LoadTexture(NeHeContext* restrict ctx, const char* restrict resourcePath, + bool flipVertical, bool genMipmaps); +SDL_GPUTexture* NeHe_CreateGPUTextureFromSurface(NeHeContext* restrict ctx, const SDL_Surface* restrict surface, + bool genMipmaps); +bool NeHe_LoadShaders(NeHeContext* restrict ctx, + SDL_GPUShader** restrict outVertex, + SDL_GPUShader** restrict outFragment, + const char* restrict name, + const NeHeShaderProgramCreateInfo* restrict info); +SDL_GPUBuffer* NeHe_CreateVertexBuffer(NeHeContext* restrict ctx, const void* restrict vertices, uint32_t verticesSize); +bool NeHe_CreateVertexIndexBuffer(NeHeContext* restrict ctx, + SDL_GPUBuffer** restrict outVertexBuffer, + SDL_GPUBuffer** restrict outIndexBuffer, + const void* restrict vertices, uint32_t verticesSize, + const void* restrict indices, uint32_t indicesSize); + +#endif//NEHE_H diff --git a/src/shaders/lesson2.metal b/src/shaders/lesson2.metal new file mode 100644 index 0000000..7bb7c8c --- /dev/null +++ b/src/shaders/lesson2.metal @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include +#include + +struct VertexInput +{ + float3 position [[attribute(0)]]; +}; + +struct VertexUniform +{ + metal::float4x4 viewproj; +}; + +struct Vertex2Fragment +{ + float4 position [[position]]; +}; + +vertex Vertex2Fragment VertexMain( + VertexInput in [[stage_in]], + constant VertexUniform& u [[buffer(0)]]) +{ + Vertex2Fragment out; + out.position = u.viewproj * float4(in.position, 1.0); + return out; +} + +fragment half4 FragmentMain(Vertex2Fragment in [[stage_in]]) +{ + return half4(1.0); +} diff --git a/src/shaders/lesson3.metal b/src/shaders/lesson3.metal new file mode 100644 index 0000000..e87d096 --- /dev/null +++ b/src/shaders/lesson3.metal @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include +#include + +struct VertexInput +{ + float3 position [[attribute(0)]]; + float4 color [[attribute(1)]]; +}; + +struct VertexUniform +{ + metal::float4x4 viewproj; +}; + +struct Vertex2Fragment +{ + float4 position [[position]]; + half4 color; +}; + +vertex Vertex2Fragment VertexMain( + VertexInput in [[stage_in]], + constant VertexUniform& u [[buffer(0)]]) +{ + Vertex2Fragment out; + out.position = u.viewproj * float4(in.position, 1.0); + out.color = half4(in.color); + return out; +} + +fragment half4 FragmentMain(Vertex2Fragment in [[stage_in]]) +{ + return in.color; +} diff --git a/src/shaders/lesson6.metal b/src/shaders/lesson6.metal new file mode 100644 index 0000000..6ef0955 --- /dev/null +++ b/src/shaders/lesson6.metal @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include +#include + +struct VertexInput +{ + float3 position [[attribute(0)]]; + float2 texcoord [[attribute(1)]]; +}; + +struct VertexUniform +{ + metal::float4x4 viewproj; + float4 color; +}; + +struct Vertex2Fragment +{ + float4 position [[position]]; + float2 texcoord; + half4 color; +}; + +vertex Vertex2Fragment VertexMain( + VertexInput in [[stage_in]], + constant VertexUniform& u [[buffer(0)]]) +{ + Vertex2Fragment out; + out.position = u.viewproj * float4(in.position, 1.0); + out.texcoord = in.texcoord; + out.color = half4(u.color); + return out; +} + +fragment half4 FragmentMain( + Vertex2Fragment in [[stage_in]], + metal::texture2d texture [[texture(0)]], + metal::sampler sampler [[sampler(0)]]) +{ + return in.color * texture.sample(sampler, in.texcoord); +} diff --git a/src/shaders/lesson7.metal b/src/shaders/lesson7.metal new file mode 100644 index 0000000..a680ad4 --- /dev/null +++ b/src/shaders/lesson7.metal @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include +#include + +struct VertexInput +{ + float3 position [[attribute(0)]]; + float2 texcoord [[attribute(1)]]; + float3 normal [[attribute(2)]]; +}; + +struct VertexUniform +{ + metal::float4x4 modelView; + metal::float4x4 projection; +}; + +struct Light +{ + float4 ambient; + float4 diffuse; + float4 position; +}; + +struct Vertex2Fragment +{ + float4 position [[position]]; + float2 texcoord; + half4 color; +}; + +vertex Vertex2Fragment VertexMain( + VertexInput in [[stage_in]], + constant VertexUniform& u [[buffer(0)]], + constant Light& light [[buffer(1)]]) +{ + const auto position = u.modelView * float4(in.position, 1.0); + const auto normal = metal::normalize(u.modelView * float4(in.normal, 0.0)).xyz; + + const auto lightVec = light.position.xyz - position.xyz; + const auto lightDist2 = metal::length_squared(lightVec); + const auto dir = metal::rsqrt(lightDist2) * lightVec; + const auto lambert = metal::max(0.0, metal::dot(normal, dir)); + + const auto ambient = 0.04 + 0.2 * half3(light.ambient.rgb); + const auto diffuse = 0.8 * half3(light.diffuse.rgb); + + Vertex2Fragment out; + out.position = u.projection * position; + out.texcoord = in.texcoord; + out.color = half4(ambient + lambert * diffuse, 1.0); + return out; +} + +fragment half4 FragmentMain( + Vertex2Fragment in [[stage_in]], + metal::texture2d texture [[texture(0)]], + metal::sampler sampler [[sampler(0)]]) +{ + return in.color * texture.sample(sampler, in.texcoord); +} diff --git a/src/shaders/lesson9.metal b/src/shaders/lesson9.metal new file mode 100644 index 0000000..6a2130f --- /dev/null +++ b/src/shaders/lesson9.metal @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +#include +#include + +struct VertexInput +{ + float3 position [[attribute(0)]]; + float2 texcoord [[attribute(1)]]; +}; + +struct VertexInstance +{ + metal::float4x4 model; + float4 color; +}; + +struct VertexUniform +{ + metal::float4x4 projection; +}; + +struct Vertex2Fragment +{ + float4 position [[position]]; + float2 texcoord; + half4 color; +}; + +vertex Vertex2Fragment VertexMain( + VertexInput in [[stage_in]], + const device VertexInstance* instance [[buffer(1)]], + constant VertexUniform& u [[buffer(0)]], + const uint instanceIdx [[instance_id]]) +{ + Vertex2Fragment out; + out.position = u.projection * instance[instanceIdx].model * float4(in.position, 1.0); + out.texcoord = in.texcoord; + out.color = half4(instance[instanceIdx].color); + return out; +} + +fragment half4 FragmentMain( + Vertex2Fragment in [[stage_in]], + metal::texture2d texture [[texture(0)]], + metal::sampler sampler [[sampler(0)]]) +{ + return in.color * texture.sample(sampler, in.texcoord); +}