Files
CavesOfSwift/Sources/JolkEngine/Renderer/OpenGL.swift
2024-05-05 17:01:56 +10:00

441 lines
10 KiB
Swift

//#set OPENGL3
import Foundation
import SDL2
#if OPENGL3
import OpenGL
#else
import OpenGL.GL3
#endif
class OpenGL: Renderer
{
private let srgb = true
private var glCtx: SDL_GLContext? = nil
private var state = OpenGLState()
struct Version { let major, minor: Int32 }
let glVersion: Version
init(version: Version)
{
self.glVersion = version
}
func create(sdlWindow: OpaquePointer) throws
{
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, Int32(SDL_GL_CONTEXT_PROFILE_COMPATIBILITY.rawValue))
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glVersion.major)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glVersion.minor)
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1)
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0)
if let context = SDL_GL_CreateContext(sdlWindow) { glCtx = context }
else { throw RendererError.sdlError(message: "SDL_GL_CreateContext: \(String(cString: SDL_GetError()))") }
guard SDL_GL_MakeCurrent(sdlWindow, glCtx) == 0
else { throw RendererError.sdlError(message: "SDL_GL_MakeCurrent: \(String(cString: SDL_GetError()))") }
state.enable([.texture2D, .cullFace, .depthTest, .rescaleNormal, .colourMaterial])
if srgb { state.enable(.frameBufferSrgb) }
state.cullFace = .back
state.clearDepth = 1
state.depthFunc = .less
state.setHint(target: .fog, hint: .dontCare)
}
func delete()
{
SDL_GL_DeleteContext(glCtx)
}
func resize(width: Int32, height: Int32)
{
glViewport(0, 0, width, height)
}
func newFrame()
{
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
}
private var _clearColour = Colour.black
var clearColour: Colour
{
get { _clearColour }
set(newColour)
{
state.clearColour(srgb ? newColour.linear : newColour)
_clearColour = newColour
}
}
func setVsync(mode: VSyncMode) throws
{
guard SDL_GL_SetSwapInterval(mode.sdlInterval) == 0
else { throw RendererError.sdlError(message: "SDL_GL_SetSwapInterval: \(String(cString: SDL_GetError()))") }
}
func createMesh(mesh: Mesh) throws -> RenderMesh
{
var buffers = [GLuint](repeating: 0, count: 2)
buffers.withUnsafeMutableBufferPointer
{
glGenBuffers(2, $0.baseAddress!)
}
state.arrayBuffer = buffers[0]
state.elementArrayBuffer = buffers[1]
glBufferData(GLenum(GL_ARRAY_BUFFER),
MemoryLayout<Mesh.Vertex>.stride * mesh.vertices.count,
mesh.vertices, GLenum(GL_STATIC_DRAW))
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
MemoryLayout<Mesh.Index>.stride * mesh.indices.count,
mesh.indices, GLenum(GL_STATIC_DRAW))
state.elementArrayBuffer = OpenGLState.defaultBuffer
state.arrayBuffer = OpenGLState.defaultBuffer
var subMeshes = [Mesh.SubMesh]()
if mesh.subMeshes.isEmpty
{
subMeshes.append(Mesh.SubMesh(start: 0, length: mesh.indices.count))
}
else
{
for subMesh in mesh.subMeshes
{
if ["Collision", "Collision3D"].contains(subMesh.key) { continue }
subMeshes.append(Mesh.SubMesh(
start: subMesh.value.start,
length: subMesh.value.length))
}
}
return RenderMesh(
vbo: Int(buffers[0]),
ibo: Int(buffers[1]),
subMeshes: subMeshes)
}
func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
{
try createTexture(data: data, width: width, height: height, filter: .linear, mipMode: .off)
}
func createTexture(data: UnsafeRawPointer, width: Int, height: Int,
filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
{
let min: Int32 = switch mipMode
{
case .off: filter.gl
case .nearest: filter.glMip
case .linear: filter.glLinearMip
}
return RenderTexture2D(try loadTexture(data: data,
width: GLsizei(width), height: GLsizei(height),
minFilter: min, magFilter: filter.gl))
}
private func loadTexture(
data: UnsafeRawPointer,
width: GLsizei, height: GLsizei,
minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR,
wrap: Int32 = GL_REPEAT) throws -> GLuint
{
// Create & upload raw bytes as texture
var texId: GLuint = 0
glGenTextures(1, &texId)
state.bindTexture2D(active: 0, texture: texId)
state.texture2DParameter(active: 0, param: .minFilter, int: minFilter)
state.texture2DParameter(active: 0, param: .magFilter, int: magFilter)
state.texture2DParameter(active: 0, param: .wrapS, int: wrap)
state.texture2DParameter(active: 0, param: .wrapT, int: wrap)
let format: GLint = srgb ? GL_SRGB8 : GL_RGBA
glTexImage2D(GLenum(GL_TEXTURE_2D),
0, format, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
// Generate mipmaps if needed
if [GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST]
.contains(minFilter) { glGenerateMipmap(GLenum(GL_TEXTURE_2D)) }
state.bindTexture2D(active: 0, texture: OpenGLState.defaultTexture)
return texId
}
func deleteMesh(_ mesh: RenderMesh)
{
var buffers = [GLuint](repeating: 0, count: 2)
buffers[0] = GLuint(mesh.iboHnd)
buffers[1] = GLuint(mesh.vboHnd)
glDeleteBuffers(1, buffers)
}
func deleteTexture(_ texture: RenderTexture2D)
{
var texId = GLuint(texture.id)
glDeleteTextures(1, &texId)
}
func setProjection(matrix: Mat4f)
{
state.matrixMode = .projection
loadMatrix(matrix)
}
func setView(matrix: Mat4f)
{
state.matrixMode = .modelView
loadMatrix(matrix)
}
func setMaterial(_ mat: Material)
{
glColor4f(mat.diffuse.r, mat.diffuse.g, mat.diffuse.b, mat.diffuse.a)
state.bindTexture2D(active: 0, texture: mat.texture.isValid ? mat.texture.id : 0)
}
private func draw(subMesh: Mesh.SubMesh)
{
glDrawElements(
GLenum(GL_TRIANGLES),
GLsizei(subMesh.length),
GLenum(GL_UNSIGNED_SHORT),
.init(bitPattern: MemoryLayout<Mesh.Index>.stride * subMesh.start));
}
private func draw(mesh: RenderMesh)
{
state.enableClient([ .vertex, .normal, .textureCoord ])
state.arrayBuffer = UInt32(mesh.vboHnd)
state.elementArrayBuffer = UInt32(mesh.iboHnd)
let stride = GLsizei(MemoryLayout<Mesh.Vertex>.stride)
glVertexPointer(3, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.position)!))
glNormalPointer(GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.normal)!))
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \Mesh.Vertex.texCoord)!))
for subMesh in mesh.subMeshes
{
draw(subMesh: subMesh)
}
state.elementArrayBuffer = OpenGLState.defaultBuffer
state.arrayBuffer = OpenGLState.defaultBuffer
state.disableClient([ .vertex, .normal, .textureCoord ])
}
func draw(mesh: RenderMesh, model: Mat4f, environment env: Environment)
{
if (mesh.subMeshes.isEmpty) { return }
applyEnvironment(env)
state.matrixMode = .modelView
glPushMatrix()
mulMatrix(model)
draw(mesh: mesh)
glPopMatrix()
}
func draw(mesh: RenderMesh, environment env: Environment)
{
if (mesh.subMeshes.isEmpty) { return }
applyEnvironment(env)
draw(mesh: mesh)
}
private func loadMatrix(_ matrix: Mat4f)
{
withUnsafePointer(to: matrix.columns)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
{ (values: UnsafePointer<GLfloat>) in
glLoadMatrixf(values)
}
}
}
private func mulMatrix(_ matrix: Mat4f)
{
withUnsafePointer(to: matrix.columns)
{
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
{ (values: UnsafePointer<GLfloat>) in
glMultMatrixf(values)
}
}
}
private func applyEnvironment(_ env: Environment)
{
let coordSource =
{ (mode: Fog.Mode) -> OpenGLState.FogCoordinateSource in
switch mode
{
case .depth: .fragmentDepth
case .distance: .coordinate
}
}
switch env.fog
{
case .none:
state.disable(.fog)
case .range(let colour, let mode, _, let start, let end):
state.fogMode = .linear
state.fogColour = srgb ? colour.linear : colour
state.fogCoodinateSource = coordSource(mode)
state.fogStart = start
state.fogEnd = end
state.enable(.fog)
break
case .factor(let colour, let mode, let type, let density):
state.fogMode = switch type
{
case .exp: .exp
case .exp2: .exp2
}
state.fogColour = srgb ? colour.linear : colour
state.fogCoodinateSource = coordSource(mode)
state.fogDensity = density
state.enable(.fog)
break
}
state.lightModelAmbient = env.ambient
let lightCaps: [OpenGLState.Capability] = [
.light0, .light1, .light2, .light3,
.light4, .light5, .light6, .light7 ]
if !env.lights.isEmpty
{
state.enable(.lighting)
for i in 0..<8
{
if i < env.lights.count
{
switch env.lights[i]
{
case .directional(let colour, let direction):
state.lightPosition[i] = Vec4f(direction, 0)
state.lightDiffuse[i] = srgb ? colour.linear : colour
case .point(let colour, let position, let intensity):
state.lightPosition[i] = Vec4f(position, 1)
state.lightDiffuse[i] = srgb ? colour.linear : colour
state.lightConstantAttenuation[i] = 0
state.lightLinearAttenuation[i] = intensity
state.lightQuadraticAttenuation[i] = 0
}
state.enable(lightCaps[i])
}
else
{
state.disable(lightCaps[i])
}
}
}
else
{
state.disable(.lighting)
for cap in lightCaps { state.disable(cap) }
}
}
func drawGizmos(lines: [Line])
{
state.disable([ .texture2D, .depthTest ])
var gizmoEnv = Environment()
gizmoEnv.fog = .none
applyEnvironment(gizmoEnv)
glBegin(GLenum(GL_LINES))
for line in lines
{
let colour = srgb ? line.colour.linear : line.colour
glColor4f(colour.r, colour.g, colour.b, colour.a)
glVertex3f(line.from.x, line.from.y, line.from.z)
glVertex3f(line.to.x, line.to.y, line.to.z)
}
glEnd()
state.enable([ .texture2D, .depthTest ])
}
}
extension VSyncMode
{
fileprivate var sdlInterval: Int32
{
switch self
{
case .off: 0
case .on: 1
case .adaptive: -1
}
}
}
extension FilterMode
{
fileprivate var gl: Int32
{
switch self
{
case .point: GL_NEAREST
case .linear: GL_LINEAR
}
}
fileprivate var glMip: Int32
{
switch self
{
case .point: GL_NEAREST_MIPMAP_NEAREST
case .linear: GL_LINEAR_MIPMAP_NEAREST
}
}
fileprivate var glLinearMip: Int32
{
switch self
{
case .point: GL_NEAREST_MIPMAP_LINEAR
case .linear: GL_LINEAR_MIPMAP_LINEAR
}
}
}
extension WrapMode
{
fileprivate var gl: Int32
{
switch self
{
case .clamping: GL_CLAMP
case .clampBorder: GL_CLAMP_TO_BORDER
case .clampEdge: GL_CLAMP_TO_EDGE
case .mirrored: GL_MIRRORED_REPEAT
case .repeating: GL_REPEAT
}
}
}