441 lines
10 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|