/* * SPDX-FileCopyrightText: (C) 2025 a dinosaur * SPDX-License-Identifier: Zlib */ import Foundation import SDLSwift import NeHe import simd struct Lesson8: AppDelegate { struct Vertex { let position: SIMD3, normal: SIMD3, texcoord: SIMD2 init(_ position: SIMD3, _ normal: SIMD3, _ texcoord: SIMD2) { self.position = position self.normal = normal self.texcoord = texcoord } } static let vertices = [ // Front Face Vertex(.init(-1.0, -1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(0.0, 0.0)), Vertex(.init( 1.0, -1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(1.0, 0.0)), Vertex(.init( 1.0, 1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(1.0, 1.0)), Vertex(.init(-1.0, 1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(0.0, 1.0)), // Back Face Vertex(.init(-1.0, -1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(1.0, 0.0)), Vertex(.init(-1.0, 1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(1.0, 1.0)), Vertex(.init( 1.0, 1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(0.0, 1.0)), Vertex(.init( 1.0, -1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(0.0, 0.0)), // Top Face Vertex(.init(-1.0, 1.0, -1.0), .init( 0.0, 1.0, 0.0), .init(0.0, 1.0)), Vertex(.init(-1.0, 1.0, 1.0), .init( 0.0, 1.0, 0.0), .init(0.0, 0.0)), Vertex(.init( 1.0, 1.0, 1.0), .init( 0.0, 1.0, 0.0), .init(1.0, 0.0)), Vertex(.init( 1.0, 1.0, -1.0), .init( 0.0, 1.0, 0.0), .init(1.0, 1.0)), // Bottom Face Vertex(.init(-1.0, -1.0, -1.0), .init( 0.0, -1.0, 0.0), .init(1.0, 1.0)), Vertex(.init( 1.0, -1.0, -1.0), .init( 0.0, -1.0, 0.0), .init(0.0, 1.0)), Vertex(.init( 1.0, -1.0, 1.0), .init( 0.0, -1.0, 0.0), .init(0.0, 0.0)), Vertex(.init(-1.0, -1.0, 1.0), .init( 0.0, -1.0, 0.0), .init(1.0, 0.0)), // Right face Vertex(.init( 1.0, -1.0, -1.0), .init( 1.0, 0.0, 0.0), .init(1.0, 0.0)), Vertex(.init( 1.0, 1.0, -1.0), .init( 1.0, 0.0, 0.0), .init(1.0, 1.0)), Vertex(.init( 1.0, 1.0, 1.0), .init( 1.0, 0.0, 0.0), .init(0.0, 1.0)), Vertex(.init( 1.0, -1.0, 1.0), .init( 1.0, 0.0, 0.0), .init(0.0, 0.0)), // Left Face Vertex(.init(-1.0, -1.0, -1.0), .init(-1.0, 0.0, 0.0), .init(0.0, 0.0)), Vertex(.init(-1.0, -1.0, 1.0), .init(-1.0, 0.0, 0.0), .init(1.0, 0.0)), Vertex(.init(-1.0, 1.0, 1.0), .init(-1.0, 0.0, 0.0), .init(1.0, 1.0)), Vertex(.init(-1.0, 1.0, -1.0), .init(-1.0, 0.0, 0.0), .init(0.0, 1.0)), ] static let indices: [UInt16] = [ 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 ] var psoUnlit: OpaquePointer? = nil, psoLight: OpaquePointer? = nil var psoBlendUnlit: OpaquePointer? = nil, psoBlendLight: OpaquePointer? = nil var vtxBuffer: OpaquePointer? = nil var idxBuffer: OpaquePointer? = nil var samplers = [OpaquePointer?](repeating: nil, count: 3) var texture: OpaquePointer? = nil var projection: simd_float4x4 = .init(1.0) struct Light { let ambient: SIMD4, diffuse: SIMD4, position: SIMD4 } var lighting = false var blending = false var light = Light( ambient: .init(0.5, 0.5, 0.5, 1.0), diffuse: .init(1.0, 1.0, 1.0, 1.0), position: .init(0.0, 0.0, 2.0, 1.0)) var filter = 0 var rot = SIMD2(repeating: 0.0) var speed = SIMD2(repeating: 0.0) var z: Float = -5.0 mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) { let (vertexShaderUnlit, fragmentShaderUnlit) = try ctx.loadShaders(name: "lesson8", vertexUniforms: 1, fragmentSamplers: 1) defer { SDL_ReleaseGPUShader(ctx.device, fragmentShaderUnlit) SDL_ReleaseGPUShader(ctx.device, vertexShaderUnlit) } let (vertexShaderLight, fragmentShaderLight) = try ctx.loadShaders(name: "lesson7", vertexUniforms: 2, fragmentSamplers: 1) defer { SDL_ReleaseGPUShader(ctx.device, fragmentShaderLight) SDL_ReleaseGPUShader(ctx.device, vertexShaderLight) } let vertexDescriptions: [SDL_GPUVertexBufferDescription] = [ SDL_GPUVertexBufferDescription( slot: 0, pitch: UInt32(MemoryLayout.stride), input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, instance_step_rate: 0), ] let vertexAttributes: [SDL_GPUVertexAttribute] = [ SDL_GPUVertexAttribute( location: 0, buffer_slot: 0, format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, offset: UInt32(MemoryLayout.offset(of: \.position)!)), SDL_GPUVertexAttribute( location: 2, buffer_slot: 0, format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, offset: UInt32(MemoryLayout.offset(of: \.normal)!)), SDL_GPUVertexAttribute( location: 1, buffer_slot: 0, format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, offset: UInt32(MemoryLayout.offset(of: \.texcoord)!)), ] var info = SDL_GPUGraphicsPipelineCreateInfo() info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST info.vertex_input_state = SDL_GPUVertexInputState( vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), num_vertex_buffers: UInt32(vertexDescriptions.count), vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), num_vertex_attributes: UInt32(vertexAttributes.count)) info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE // Common pipeline depth & colour target options var colorTargets = [ SDL_GPUColorTargetDescription() ] colorTargets[0].format = SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) info.target_info.color_target_descriptions = colorTargets.withUnsafeBufferPointer(\.baseAddress!) info.target_info.num_color_targets = UInt32(colorTargets.count) info.target_info.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM info.target_info.has_depth_stencil_target = true info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL // Setup depth/stencil & colour pipeline state for no blending info.depth_stencil_state.enable_depth_test = true info.depth_stencil_state.enable_depth_write = true colorTargets[0].blend_state = SDL_GPUColorTargetBlendState() // Create unlit pipeline info.vertex_shader = vertexShaderUnlit info.fragment_shader = fragmentShaderUnlit guard let psoUnlit = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else { throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) } self.psoUnlit = psoUnlit // Create lit pipeline info.vertex_shader = vertexShaderLight info.fragment_shader = fragmentShaderLight guard let psoLight = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else { throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) } self.psoLight = psoLight // Setup depth/stencil & colour pipeline state for blending info.depth_stencil_state.enable_depth_test = false info.depth_stencil_state.enable_depth_write = false colorTargets[0].blend_state.enable_blend = true colorTargets[0].blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD colorTargets[0].blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD colorTargets[0].blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA colorTargets[0].blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE colorTargets[0].blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA colorTargets[0].blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE // Create blended unlit pipeline info.vertex_shader = vertexShaderUnlit info.fragment_shader = fragmentShaderUnlit guard let psoBlendUnlit = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else { throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) } self.psoBlendUnlit = psoBlendUnlit // Create blended lit pipeline info.vertex_shader = vertexShaderLight info.fragment_shader = fragmentShaderLight guard let psoBlendLight = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else { throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) } self.psoBlendLight = psoBlendLight func createSampler( filter: SDL_GPUFilter, mipMode: SDL_GPUSamplerMipmapMode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, maxLod: Float = 0.0) throws(NeHeError) -> OpaquePointer { var samplerInfo = SDL_GPUSamplerCreateInfo() samplerInfo.min_filter = filter samplerInfo.mag_filter = filter samplerInfo.mipmap_mode = mipMode samplerInfo.max_lod = maxLod guard let sampler = SDL_CreateGPUSampler(ctx.device, &samplerInfo) else { throw .sdlError("SDL_CreateGPUSampler", String(cString: SDL_GetError())) } return sampler } self.samplers[0] = try createSampler(filter: SDL_GPU_FILTER_NEAREST) self.samplers[1] = try createSampler(filter: SDL_GPU_FILTER_LINEAR) self.samplers[2] = try createSampler(filter: SDL_GPU_FILTER_LINEAR, maxLod: .greatestFiniteMagnitude) try ctx.copyPass { (pass) throws(NeHeError) in self.texture = try pass.createTextureFrom(bmpResource: "Glass", flip: true, genMipmaps: true) self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) } } func quit(ctx: NeHeContext) { SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) SDL_ReleaseGPUTexture(ctx.device, self.texture) self.samplers.reversed().forEach { SDL_ReleaseGPUSampler(ctx.device, $0) } SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.psoBlendLight) SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.psoBlendUnlit) SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.psoLight) SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.psoUnlit) } mutating func resize(size: Size) { let aspect = Float(size.width) / Float(max(1, size.height)) self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) } mutating func draw(ctx: inout NeHeContext, cmd: OpaquePointer, swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) { var colorInfo = SDL_GPUColorTargetInfo() colorInfo.texture = swapchain colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) colorInfo.load_op = SDL_GPU_LOADOP_CLEAR colorInfo.store_op = SDL_GPU_STOREOP_STORE var depthInfo = SDL_GPUDepthStencilTargetInfo() depthInfo.texture = ctx.depthTexture depthInfo.clear_depth = 1.0 depthInfo.load_op = SDL_GPU_LOADOP_CLEAR depthInfo.store_op = SDL_GPU_STOREOP_DONT_CARE depthInfo.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE depthInfo.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE depthInfo.cycle = true // Begin pass & bind pipeline state let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo) let pipeline = switch (self.blending, self.lighting) { case (false, false): self.psoUnlit case (false, true): self.psoLight case (true, false): self.psoBlendUnlit case (true, true): self.psoBlendLight } SDL_BindGPUGraphicsPipeline(pass, pipeline) // Bind texture var textureBinding = SDL_GPUTextureSamplerBinding(texture: self.texture, sampler: self.samplers[self.filter]) SDL_BindGPUFragmentSamplers(pass, 0, &textureBinding, 1) // Bind vertex & index buffers let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) SDL_BindGPUVertexBuffers(pass, 0, vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) // Setup the cube's model matrix var model: simd_float4x4 = .translation(.init(0.0, 0.0, self.z)) model.rotate(angle: rot.x, axis: .init(1.0, 0.0, 0.0)) model.rotate(angle: rot.y, axis: .init(0.0, 1.0, 0.0)) // Push shader uniforms if self.lighting { struct Uniforms { var model: simd_float4x4, projection: simd_float4x4 } var u = Uniforms( model: model, projection: self.projection) SDL_PushGPUVertexUniformData(cmd, 0, &u, UInt32(MemoryLayout.size)) SDL_PushGPUVertexUniformData(cmd, 1, &self.light, UInt32(MemoryLayout.size)) } else { struct Uniforms { var modelViewProj: simd_float4x4, color: SIMD4 } var u = Uniforms( modelViewProj: self.projection * model, color: .init(1.0, 1.0, 1.0, 0.5)) // 50% translucency SDL_PushGPUVertexUniformData(cmd, 0, &u, UInt32(MemoryLayout.size)) } // Draw textured cube SDL_DrawGPUIndexedPrimitives(pass, UInt32(Self.indices.count), 1, 0, 0, 0) SDL_EndGPURenderPass(pass) let keys = SDL_GetKeyboardState(nil)! if keys[Int(SDL_SCANCODE_PAGEUP.rawValue)] { self.z -= 0.02 } if keys[Int(SDL_SCANCODE_PAGEDOWN.rawValue)] { self.z += 0.02 } if keys[Int(SDL_SCANCODE_UP.rawValue)] { speed.x -= 0.01 } if keys[Int(SDL_SCANCODE_DOWN.rawValue)] { speed.x += 0.01 } if keys[Int(SDL_SCANCODE_RIGHT.rawValue)] { speed.y += 0.1 } if keys[Int(SDL_SCANCODE_LEFT.rawValue)] { speed.y -= 0.1 } self.rot += self.speed } mutating func key(ctx: inout NeHeContext, key: SDL_Keycode, down: Bool, repeat: Bool) { guard down && !`repeat` else { return } switch key { case SDLK_L: self.lighting = !self.lighting case SDLK_B: self.blending = !self.blending case SDLK_F: self.filter = (self.filter + 1) % self.samplers.count default: break } } } @main struct Program: AppRunner { typealias Delegate = Lesson8 static let config = AppConfig( title: "Tom Stanis & NeHe's Blending Tutorial", width: 640, height: 480, createDepthBuffer: SDL_GPU_TEXTUREFORMAT_D16_UNORM, bundle: Bundle.module) }