531 lines
14 KiB
Swift
531 lines
14 KiB
Swift
//#set OPENGL3
|
|
|
|
import Foundation
|
|
import Maths
|
|
import SDL2
|
|
import OpenGL
|
|
#if OPENGL3
|
|
import OpenGL.GL
|
|
#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
|
|
}
|
|
}
|
|
|
|
var wireframe: Bool
|
|
{
|
|
get { state.polygonMode == .line }
|
|
set(mode) { state.polygonMode = mode ? .line : .fill }
|
|
}
|
|
|
|
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<V: Vertex>(mesh: Mesh<V>) throws -> RenderMesh<V>
|
|
{
|
|
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<V>.stride * mesh.vertices.count,
|
|
mesh.vertices, GLenum(GL_STATIC_DRAW))
|
|
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
|
|
MemoryLayout<V>.stride * mesh.indices.count,
|
|
mesh.indices, GLenum(GL_STATIC_DRAW))
|
|
|
|
state.elementArrayBuffer = OpenGLState.defaultBuffer
|
|
state.arrayBuffer = OpenGLState.defaultBuffer
|
|
|
|
var subMeshes = [Mesh<V>.SubMesh]()
|
|
if mesh.subMeshes.isEmpty
|
|
{
|
|
subMeshes.append(Mesh<V>.SubMesh(start: 0, length: mesh.indices.count))
|
|
}
|
|
else
|
|
{
|
|
for subMesh in mesh.subMeshes
|
|
{
|
|
subMeshes.append(Mesh<V>.SubMesh(
|
|
start: subMesh.value.start,
|
|
length: subMesh.value.length,
|
|
material: subMesh.value.material))
|
|
}
|
|
}
|
|
|
|
return RenderMesh(
|
|
vbo: Int(buffers[0]),
|
|
ibo: Int(buffers[1]),
|
|
subMeshes: subMeshes,
|
|
materials: mesh.materials)
|
|
}
|
|
|
|
func createTexture(image: Image) throws -> RenderTexture2D
|
|
{
|
|
try createTexture(image: image, filter: .linear, mipMode: .off)
|
|
}
|
|
|
|
func createTexture(image: Image, filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
|
|
{
|
|
let min: Int32 = switch mipMode
|
|
{
|
|
case .off: filter.gl
|
|
case .nearest: filter.glMip
|
|
case .linear: filter.glLinearMip
|
|
}
|
|
return try image.data.withUnsafeBytes
|
|
{ raw in
|
|
RenderTexture2D(try loadTexture(
|
|
data: raw.baseAddress!, format: image.format,
|
|
width: GLsizei(image.width), height: GLsizei(image.height),
|
|
mipLevels: image.mipLevels,
|
|
minFilter: min, magFilter: filter.gl))
|
|
}
|
|
}
|
|
|
|
private func loadTexture(
|
|
data: UnsafeRawPointer, format: Image.Format,
|
|
width: GLsizei, height: GLsizei, mipLevels: Int,
|
|
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 upload = getTextureUploader(format: format)
|
|
var offset = 0
|
|
for i in 0...mipLevels
|
|
{
|
|
let levelWidth = width &>> i, levelHeight = height &>> i
|
|
let size = format.computeSize(width: UInt32(levelWidth), height: UInt32(levelHeight))
|
|
upload(GLint(i), levelWidth, levelHeight, data + offset, GLsizei(size))
|
|
offset += size
|
|
}
|
|
|
|
// Generate mipmaps if needed
|
|
if [GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST]
|
|
.contains(minFilter) && mipLevels == 0 { glGenerateMipmap(GLenum(GL_TEXTURE_2D)) }
|
|
|
|
state.bindTexture2D(active: 0, texture: OpenGLState.defaultTexture)
|
|
|
|
return texId
|
|
}
|
|
|
|
private typealias UploadFunc = (GLint, GLsizei, GLsizei, UnsafeRawPointer, GLsizei) -> Void
|
|
private func getTextureUploader(format: Image.Format) -> UploadFunc
|
|
{
|
|
let target = GLenum(GL_TEXTURE_2D)
|
|
let border: GLint = 0
|
|
return switch format
|
|
{
|
|
case .argb8888: { (level, width, height, data, size) in
|
|
glTexImage2D(target, level, GLint(self.srgb ? GL_SRGB8 : GL_RGBA),
|
|
width, height, border, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
|
|
}
|
|
case .s3tc_bc1: { (level, width, height, data, size) in
|
|
glCompressedTexImage2D(target, level, GLenum(GL_COMPRESSED_SRGB_S3TC_DXT1_EXT),
|
|
width, height, border, size, data)
|
|
}
|
|
case .rgtc_bc5_3dc: { (level, width, height, data, size) in
|
|
glCompressedTexImage2D(target, level, GLenum(GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT),
|
|
width, height, border, size, data)
|
|
}
|
|
default: fatalError()
|
|
}
|
|
}
|
|
|
|
|
|
func deleteMesh<V: Vertex>(_ mesh: RenderMesh<V>)
|
|
{
|
|
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.materialDiffuse = srgb ? mat.diffuse.linear : mat.diffuse
|
|
state.materialSpecular = srgb ? mat.specular.linear : mat.specular
|
|
state.materialEmmision = srgb ? mat.emmision.linear : mat.emmision
|
|
state.materialShininess = mat.specularExp
|
|
state.bindTexture2D(active: 0, texture: mat.texture.isValid ? mat.texture.id : 0)
|
|
}
|
|
|
|
|
|
private func draw<V: Vertex>(subMesh: Mesh<V>.SubMesh)
|
|
{
|
|
glDrawElements(
|
|
GLenum(GL_TRIANGLES),
|
|
GLsizei(subMesh.length),
|
|
GLenum(GL_UNSIGNED_SHORT),
|
|
.init(bitPattern: MemoryLayout<Mesh<V>.Index>.stride * subMesh.start));
|
|
}
|
|
|
|
private func bindMesh<V: Vertex>(mesh: RenderMesh<V>)
|
|
{
|
|
state.arrayBuffer = UInt32(mesh.vboHnd)
|
|
state.elementArrayBuffer = UInt32(mesh.iboHnd)
|
|
|
|
let stride = GLsizei(MemoryLayout<V>.stride)
|
|
if V.self == VertexPositionNormalTexcoord.self
|
|
{
|
|
state.enableClient([ .vertex, .normal, .textureCoord ])
|
|
glVertexPointer(3, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.position)!))
|
|
glNormalPointer(GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.normal)!))
|
|
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.texCoord)!))
|
|
}
|
|
else if V.self == VertexPositionNormalColourTexcoord.self
|
|
{
|
|
state.enableClient([ .vertex, .colour, .normal, .textureCoord ])
|
|
glVertexPointer(3, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.position)!))
|
|
glColorPointer(4, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.colour)!))
|
|
glNormalPointer(GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.normal)!))
|
|
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.texCoord)!))
|
|
}
|
|
else if V.self == VMeshVertex.self
|
|
{
|
|
state.enableClient([ .vertex, .normal, .textureCoord ])
|
|
glVertexPointer(3, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VMeshVertex.position)!))
|
|
glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VMeshVertex.texCoord)!))
|
|
glNormalPointer(GLenum(GL_BYTE), stride,
|
|
UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VMeshVertex.normal)!))
|
|
}
|
|
}
|
|
|
|
private func unbindMesh()
|
|
{
|
|
state.elementArrayBuffer = OpenGLState.defaultBuffer
|
|
state.arrayBuffer = OpenGLState.defaultBuffer
|
|
|
|
state.disableClient([ .vertex, .colour, .normal, .textureCoord ])
|
|
}
|
|
|
|
private func draw<V: Vertex>(mesh: RenderMesh<V>)
|
|
{
|
|
bindMesh(mesh: mesh)
|
|
for subMesh in mesh.subMeshes
|
|
{
|
|
draw(subMesh: subMesh)
|
|
}
|
|
unbindMesh()
|
|
}
|
|
|
|
func draw<V: Vertex>(mesh: RenderMesh<V>, model: Mat4f, environment env: Environment)
|
|
{
|
|
if (mesh.subMeshes.isEmpty) { return }
|
|
|
|
applyEnvironment(env)
|
|
state.matrixMode = .modelView
|
|
glPushMatrix()
|
|
mulMatrix(model)
|
|
draw(mesh: mesh)
|
|
glPopMatrix()
|
|
}
|
|
|
|
func draw<V: Vertex>(mesh: RenderMesh<V>, environment env: Environment)
|
|
{
|
|
if (mesh.subMeshes.isEmpty) { return }
|
|
|
|
applyEnvironment(env)
|
|
draw(mesh: mesh)
|
|
}
|
|
|
|
func draw<V: Vertex>(mesh: RenderMesh<V>, subMesh: Mesh<V>.SubMesh, environment env: Environment)
|
|
{
|
|
applyEnvironment(env)
|
|
bindMesh(mesh: mesh)
|
|
draw(subMesh: subMesh)
|
|
unbindMesh()
|
|
}
|
|
|
|
private func loadMatrix(_ matrix: Mat4f)
|
|
{
|
|
//withUnsafePointer(to: matrix.columns)
|
|
withUnsafePointer(to: matrix)
|
|
{
|
|
$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
|
|
{ (values: UnsafePointer<GLfloat>) in
|
|
glLoadMatrixf(values)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func mulMatrix(_ matrix: Mat4f)
|
|
{
|
|
//withUnsafePointer(to: matrix.columns)
|
|
withUnsafePointer(to: matrix)
|
|
{
|
|
$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
|
|
state.lightModelLocalViewer = true
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|