c: Add lessons 1-10

This commit is contained in:
2025-05-31 18:09:29 +10:00
commit cdb2c800cc
37 changed files with 4420 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@@ -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

8
CMakeLists.txt Normal file
View File

@@ -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)

View File

@@ -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
$<$<C_COMPILER_ID:Clang,AppleClang>:-Weverything -Wno-declaration-after-statement -Wno-padded -Wno-switch-enum -Wno-cast-qual>
$<$<C_COMPILER_ID:GNU>:-Wall -Wextra -pedantic>
$<$<C_COMPILER_ID:MSVC>:/W4>)
target_compile_definitions(${target} PRIVATE
$<$<C_COMPILER_ID:MSVC>:_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()

BIN
data/Crate.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
data/Glass.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
data/Mud.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
data/NeHe.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
data/Star.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

160
data/World.txt Normal file
View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

186
scripts/compile_shaders.py Executable file
View File

@@ -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()

12
src/c/CMakeLists.txt Normal file
View File

@@ -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)

211
src/c/application.c Normal file
View File

@@ -0,0 +1,211 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include "nehe.h"
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL_main.h>
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

22
src/c/application.h Normal file
View File

@@ -0,0 +1,22 @@
#ifndef APPLICATION_H
#define APPLICATION_H
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_keycode.h>
#include <stdbool.h>
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

34
src/c/lesson1.c Normal file
View File

@@ -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
};

392
src/c/lesson10.c Normal file
View File

@@ -0,0 +1,392 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include "nehe.h"
#include <float.h>
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
};

182
src/c/lesson2.c Normal file
View File

@@ -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
};

189
src/c/lesson3.c Normal file
View File

@@ -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
};

197
src/c/lesson4.c Normal file
View File

@@ -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
};

245
src/c/lesson5.c Normal file
View File

@@ -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
};

266
src/c/lesson6.c Normal file
View File

@@ -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
};

377
src/c/lesson7.c Normal file
View File

@@ -0,0 +1,377 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include "nehe.h"
#include <float.h>
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
};

400
src/c/lesson8.c Normal file
View File

@@ -0,0 +1,400 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include "nehe.h"
#include <float.h>
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
};

359
src/c/lesson9.c Normal file
View File

@@ -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
};

132
src/c/matrix.c Normal file
View File

@@ -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];
}

36
src/c/matrix.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef MATRIX_H
#define MATRIX_H
#include <SDL3/SDL_stdinc.h>
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

649
src/c/nehe.c Normal file
View File

@@ -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;
}

62
src/c/nehe.h Normal file
View File

@@ -0,0 +1,62 @@
#ifndef NEHE_H
#define NEHE_H
#include "application.h"
#include "matrix.h"
#include <SDL3/SDL.h>
#include <stdint.h>
#include <stddef.h>
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

36
src/shaders/lesson2.metal Normal file
View File

@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include <metal_stdlib>
#include <simd/simd.h>
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);
}

39
src/shaders/lesson3.metal Normal file
View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include <metal_stdlib>
#include <simd/simd.h>
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;
}

45
src/shaders/lesson6.metal Normal file
View File

@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include <metal_stdlib>
#include <simd/simd.h>
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<half, metal::access::sample> texture [[texture(0)]],
metal::sampler sampler [[sampler(0)]])
{
return in.color * texture.sample(sampler, in.texcoord);
}

65
src/shaders/lesson7.metal Normal file
View File

@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include <metal_stdlib>
#include <simd/simd.h>
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<half, metal::access::sample> texture [[texture(0)]],
metal::sampler sampler [[sampler(0)]])
{
return in.color * texture.sample(sampler, in.texcoord);
}

52
src/shaders/lesson9.metal Normal file
View File

@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
#include <metal_stdlib>
#include <simd/simd.h>
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<half, metal::access::sample> texture [[texture(0)]],
metal::sampler sampler [[sampler(0)]])
{
return in.color * texture.sample(sampler, in.texcoord);
}