diff --git a/src/main/resources/ocelot/desktop/shader/general_3d.frag b/src/main/resources/ocelot/desktop/shader/general_3d.frag new file mode 100644 index 0000000..943d033 --- /dev/null +++ b/src/main/resources/ocelot/desktop/shader/general_3d.frag @@ -0,0 +1,25 @@ +#version 140 + +varying vec4 fColor; +varying vec3 fNormal; +varying vec2 fUV; + +uniform vec3 uLightDir; +uniform vec3 uLightColor; +uniform vec3 uAmbientLightColor; + +uniform sampler2D uTexture; + +void main() { + vec3 normal = normalize(fNormal); + + vec4 texColor = texture2D(uTexture, fUV); + vec3 albedo = vec3(1.0, 0.0, 0.0); + float alpha = 1.0; + + vec3 light = uAmbientLightColor; + light += clamp(dot(uLightDir, normal), 0.0, 1.0) * uLightColor; + vec3 color = light * albedo; + + gl_FragColor = vec4(color, alpha); +} \ No newline at end of file diff --git a/src/main/resources/ocelot/desktop/shader/general_3d.vert b/src/main/resources/ocelot/desktop/shader/general_3d.vert new file mode 100644 index 0000000..6785ca9 --- /dev/null +++ b/src/main/resources/ocelot/desktop/shader/general_3d.vert @@ -0,0 +1,30 @@ +#version 140 + +attribute vec3 inPos; +attribute vec3 inNormal; +attribute vec2 inUV; + +attribute vec4 inColor; +attribute vec4 inTransform0; +attribute vec4 inTransform1; +attribute vec4 inTransform2; +attribute vec3 inUVTransform0; +attribute vec3 inUVTransform1; + +varying vec4 fColor; +varying vec3 fNormal; +varying vec2 fUV; + +uniform mat4 uView; +uniform mat4 uProj; + +void main() { + mat4 transform = transpose(mat4(inTransform0, inTransform1, inTransform2, vec4(0, 0, 0, 1))); + fNormal = mat3(transpose(inverse(transform))) * inNormal; + gl_Position = uProj * uView * transform * vec4(inPos, 1); + + mat3 uvTransform = transpose(mat3(inUVTransform0, inUVTransform1, vec3(0, 0, 1))); + fUV = (uvTransform * vec3(inUV, 1)).xy; + + fColor = inColor; +} \ No newline at end of file diff --git a/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala b/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala index c7dce71..8d175df 100644 --- a/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala +++ b/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala @@ -1,5 +1,7 @@ package ocelot.desktop.color +import ocelot.desktop.geometry.Vector3D + case class RGBAColorNorm(r: Float, g: Float, b: Float, a: Float = 1f) extends Color { assert(0 <= r && r <= 1.0f, "Invalid RED channel") assert(0 <= g && g <= 1.0f, "Invalid GREEN channel") @@ -8,6 +10,8 @@ case class RGBAColorNorm(r: Float, g: Float, b: Float, a: Float = 1f) extends Co def components: Array[Float] = Array(r, g, b, a) + def rgbVector: Vector3D = Vector3D(r, g, b) + def mapA(f: Float => Float): RGBAColorNorm = copy(a = f(a)) override def toInt: IntColor = toRGBA.toInt diff --git a/src/main/scala/ocelot/desktop/geometry/Basis3D.scala b/src/main/scala/ocelot/desktop/geometry/Basis3D.scala index 94ce3d1..94c9ef5 100644 --- a/src/main/scala/ocelot/desktop/geometry/Basis3D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Basis3D.scala @@ -10,6 +10,13 @@ object Basis3D { def scale(v: Vector3D): Basis3D = Basis3D(Vector3D(v.x, 0, 0), Vector3D(0, v.y, 0), Vector3D(0, 0, v.z)) def scale(s: Float): Basis3D = Basis3D(Vector3D(s, 0, 0), Vector3D(0, s, 0), Vector3D(0, 0, s)) + + def lookingAt(target: Vector3D, up: Vector3D = Vector3D.Up): Basis3D = { + val v_z = target.normalize + val v_x = up.cross(v_z).normalize + val v_y = v_z.cross(v_x) + Basis3D(v_x, v_y, v_z) + } } case class Basis3D(x: Vector3D, y: Vector3D, z: Vector3D) { @@ -67,5 +74,17 @@ case class Basis3D(x: Vector3D, y: Vector3D, z: Vector3D) { ) } + def up: Vector3D = this * Vector3D.Up + + def down: Vector3D = this * Vector3D.Down + + def left: Vector3D = this * Vector3D.Left + + def right: Vector3D = this * Vector3D.Right + + def forward: Vector3D = this * Vector3D.Forward + + def back: Vector3D = this * Vector3D.Back + override def toString: String = s"Basis3D [$x, $y, $z]" } diff --git a/src/main/scala/ocelot/desktop/geometry/Box3D.scala b/src/main/scala/ocelot/desktop/geometry/Box3D.scala new file mode 100644 index 0000000..9924fce --- /dev/null +++ b/src/main/scala/ocelot/desktop/geometry/Box3D.scala @@ -0,0 +1,34 @@ +package ocelot.desktop.geometry + +object Box3D { + def fromMinMax(min: Vector3D, max: Vector3D): Box3D = { + val center = (min + max) * 0.5f + val halfExtents = (max - min) * 0.5f + Box3D(center, halfExtents) + } + + def fromVertices(vertices: Iterator[Vector3D]): Box3D = { + val first = vertices.next() + val (min, max) = vertices.foldLeft((first, first)) { case ((min, max), vertex) => + (min.min(vertex), max.max(vertex)) + } + Box3D.fromMinMax(min, max) + } +} + +case class Box3D(center: Vector3D, halfExtents: Vector3D) { + def vertices: Seq[Vector3D] = Seq( + center + halfExtents * Vector3D(-1, -1, -1), + center + halfExtents * Vector3D(-1, -1, 1), + center + halfExtents * Vector3D(-1, 1, -1), + center + halfExtents * Vector3D(-1, 1, 1), + center + halfExtents * Vector3D(1, -1, -1), + center + halfExtents * Vector3D(1, -1, 1), + center + halfExtents * Vector3D(1, 1, -1), + center + halfExtents * Vector3D(1, 1, 1), + ) + + def transform(transform: Transform3D): Box3D = { + Box3D.fromVertices(vertices.iterator.map(transform * _)) + } +} diff --git a/src/main/scala/ocelot/desktop/geometry/ProjectionMatrix3D.scala b/src/main/scala/ocelot/desktop/geometry/ProjectionMatrix3D.scala index bde2ebe..c8da411 100644 --- a/src/main/scala/ocelot/desktop/geometry/ProjectionMatrix3D.scala +++ b/src/main/scala/ocelot/desktop/geometry/ProjectionMatrix3D.scala @@ -2,7 +2,7 @@ package ocelot.desktop.geometry object ProjectionMatrix3D { def perspective(aspect: Float, fovY: Float, zNear: Float, zFar: Float): ProjectionMatrix3D = { - val f = (1.0 / math.tan(fovY / 2.0)).toFloat + val f = (1.0 / math.tan(math.toRadians(fovY) / 2.0)).toFloat ProjectionMatrix3D( f / aspect, 0, 0, 0, 0, f, 0, 0, diff --git a/src/main/scala/ocelot/desktop/geometry/Transform3D.scala b/src/main/scala/ocelot/desktop/geometry/Transform3D.scala index a7b8256..5bbf14f 100644 --- a/src/main/scala/ocelot/desktop/geometry/Transform3D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Transform3D.scala @@ -14,6 +14,11 @@ object Transform3D { def scale(s: Float): Transform3D = Transform3D(Basis3D.scale(s)) def translate(v: Vector3D): Transform3D = Transform3D(Basis3D.identity, v) + + def lookingAt(origin: Vector3D, target: Vector3D, up: Vector3D = Vector3D.Up): Transform3D = { + val basis = Basis3D.lookingAt(origin - target, up) + Transform3D(basis, origin) + } } case class Transform3D(basis: Basis3D, origin: Vector3D) { diff --git a/src/main/scala/ocelot/desktop/geometry/Vector3D.scala b/src/main/scala/ocelot/desktop/geometry/Vector3D.scala index 9250976..b55c84e 100644 --- a/src/main/scala/ocelot/desktop/geometry/Vector3D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Vector3D.scala @@ -2,10 +2,20 @@ package ocelot.desktop.geometry object Vector3D { val Zero: Vector3D = Vector3D(0, 0, 0) + val AxisX: Vector3D = Vector3D(1, 0, 0) val AxisY: Vector3D = Vector3D(0, 1, 0) val AxisZ: Vector3D = Vector3D(0, 0, 1) + val Up: Vector3D = AxisY + val Down: Vector3D = -AxisY + + val Left: Vector3D = -AxisX + val Right: Vector3D = AxisX + + val Forward: Vector3D = -AxisZ + val Back: Vector3D = AxisZ + def apply(x: Double, y: Double, z: Double): Vector3D = Vector3D(x.toFloat, y.toFloat, z.toFloat) } @@ -26,6 +36,12 @@ case class Vector3D(x: Float, y: Float, z: Float) { def dot(that: Vector3D): Float = x * that.x + y * that.y + z * that.z + def cross(that: Vector3D): Vector3D = Vector3D( + y * that.z - z * that.y, + z * that.x - x * that.z, + x * that.y - y * that.x + ) + def length: Float = math.sqrt(lengthSquared).toFloat def lengthSquared: Float = x * x + y * y + z * z diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index 10b8140..0bf9679 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -21,7 +21,7 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { private var height = 600 private val shaderProgram = new ShaderProgram("general") - private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, shaderProgram) + private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) private val normalFont = new Font("unscii-16", 16) private val smallFont = new Font("unscii-8", 8) @@ -39,7 +39,9 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, offscreenFramebuffer) GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, offscreenTexture.texture, 0) - GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0) + + private var currentFramebuffer = 0 + private val currentScissor: Option[Rect2D] = None shaderProgram.set("uTexture", 0) shaderProgram.set("uTextTexture", 1) @@ -62,10 +64,10 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { offscreenTexture.bind() GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, null.asInstanceOf[ByteBuffer]) - GL11.glViewport(0, 0, width, height) viewportChanged = true } + viewportChanged } @@ -97,9 +99,6 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { _font = smallFont } - GL11.glEnable(GL11.GL_BLEND) - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA) - def save(): Unit = { stack.push(stack.head.copy()) } @@ -126,47 +125,42 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { GL30.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, 0) GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, offscreenFramebuffer) GL30.glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST) + currentFramebuffer = offscreenFramebuffer } def endGroupAlpha(alpha: Float): Unit = { flush() - GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0) - offscreenTexture.bind() + currentFramebuffer = 0 foreground = RGBAColorNorm(1, 1, 1, alpha) spriteRect = Rect2D(0, 1f, 1f, -1f) - _rect(0, 0, width / scalingFactor, height / scalingFactor, fixUV = false) - renderer.flush() + flush(mainTexture = offscreenTexture) } - def startViewport(): Unit = { + def blitViewport3D(viewport: Viewport3D, bounds: Rect2D, alpha: Float = 1.0f): Unit = { + flush() + foreground = RGBAColorNorm(1, 1, 1, alpha) + spriteRect = Rect2D(0, 1f, 1f, -1f) + _rect(bounds.x, bounds.y, bounds.w, bounds.h, fixUV = false) + flush(mainTexture = viewport.textureColor) + } + + def begin(): Unit = { shaderProgram.set("uProj", Transform2D.viewport(width, height)) } def clear(): Unit = { - flush() - GL11.glClearColor(0.95f, 0.95f, 0.95f, 1.0f) - GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) + flush(clear = true) } def setScissor(x: Float, y: Float, width: Float, height: Float): Unit = { flush() stack.head.scissor = Some((x, y, width * scalingFactor, height * scalingFactor)) - // TODO: make them aware of the transform stack - GL11.glScissor( - Math.round(x * scalingFactor), - Math.round(this.height - height * scalingFactor - y * scalingFactor), - Math.round(width * scalingFactor), - Math.round(height * scalingFactor) - ) - GL11.glEnable(GL11.GL_SCISSOR_TEST) } - def setScissor(): Unit = { flush() stack.head.scissor = None - GL11.glDisable(GL11.GL_SCISSOR_TEST) } def clear(x: Int, y: Int, width: Int, height: Int): Unit = { @@ -373,10 +367,37 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { line(start.x, start.y, end.x, end.y, thickness, color) } - def flush(): Unit = { - Spritesheet.texture.bind() + def flush(mainTexture: Texture = Spritesheet.texture, clear: Boolean = false): Unit = { + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, currentFramebuffer) + GL11.glViewport(0, 0, width, height) + + GL11.glDisable(GL11.GL_DEPTH_TEST) + GL11.glDisable(GL11.GL_CULL_FACE) + + GL11.glEnable(GL11.GL_BLEND) + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA) + + stack.head.scissor match { + case Some((x, y, w, h)) => + GL11.glEnable(GL11.GL_SCISSOR_TEST) + GL11.glScissor( + Math.round(x * scalingFactor), + Math.round(height - h * scalingFactor - y * scalingFactor), + Math.round(w * scalingFactor), + Math.round(h * scalingFactor) + ) + case _ => + GL11.glDisable(GL11.GL_SCISSOR_TEST) + } + + mainTexture.bind() _font.texture.bind(1) renderer.flush() + + if (clear) { + GL11.glClearColor(1, 1, 1, 1) + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) + } } def update(): Unit = { diff --git a/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala b/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala index 15650b9..2cc2888 100644 --- a/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala +++ b/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala @@ -1,6 +1,6 @@ package ocelot.desktop.graphics -import ocelot.desktop.geometry.Transform2D +import ocelot.desktop.geometry.{ProjectionMatrix3D, Transform2D, Transform3D, Vector3D} import ocelot.desktop.util.{Logging, Resource} import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL20 @@ -51,6 +51,11 @@ class ShaderProgram(name: String) extends Logging with Resource { GL20.glUniform1i(getLocation(name), texture.texture) } + def set(name: String, vector: Vector3D): Unit = { + bind() + GL20.glUniform3f(getLocation(name), vector.x, vector.y, vector.z) + } + def set(name: String, transform: Transform2D): Unit = { bind() @@ -61,6 +66,25 @@ class ShaderProgram(name: String) extends Logging with Resource { GL20.glUniformMatrix3(getLocation(name), true, buffer) } + def set(name: String, transform: Transform3D): Unit = { + bind() + + val array = transform.array ++ Array(0f, 0f, 0f, 1f) + val buffer = BufferUtils.createFloatBuffer(array.length) + buffer.put(array) + buffer.flip() + GL20.glUniformMatrix4(getLocation(name), true, buffer) + } + + def set(name: String, projection: ProjectionMatrix3D): Unit = { + bind() + + val buffer = BufferUtils.createFloatBuffer(projection.array.length) + buffer.put(projection.array) + buffer.flip() + GL20.glUniformMatrix4(getLocation(name), true, buffer) + } + def set(name: String, v: Int): Unit = { bind() GL20.glUniform1i(getLocation(name), v) diff --git a/src/main/scala/ocelot/desktop/graphics/Viewport3D.scala b/src/main/scala/ocelot/desktop/graphics/Viewport3D.scala index f687a5d..139a68d 100644 --- a/src/main/scala/ocelot/desktop/graphics/Viewport3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/Viewport3D.scala @@ -1,9 +1,15 @@ package ocelot.desktop.graphics +import ocelot.desktop.graphics.mesh.{Mesh3D, MeshInstance3D, MeshVertex3D} +import ocelot.desktop.graphics.render.InstanceRenderer +import ocelot.desktop.graphics.scene.{Scene3D, SceneMesh3D} +import ocelot.desktop.util.Spritesheet import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL30} +import scala.collection.mutable + class Viewport3D(val width: Int, var height: Int) { - private val textureColor = new Texture(width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE) + private[graphics] val textureColor = new Texture(width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE) private val textureDepth = new Texture(width, height, GL11.GL_DEPTH_COMPONENT, GL11.GL_FLOAT) private val framebuffer = ARBFramebufferObject.glGenFramebuffers() @@ -11,4 +17,117 @@ class Viewport3D(val width: Int, var height: Int) { GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, textureColor.texture, 0) GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL11.GL_TEXTURE_2D, textureDepth.texture, 0) GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0) + + private type Instancer = InstanceRenderer[MeshVertex3D, MeshInstance3D] + + private val shaderProgram: ShaderProgram = new ShaderProgram("general_3d") + shaderProgram.set("uTexture", 0) + + private val opaqueInstancers = new mutable.HashMap[Mesh3D, Instancer]() + private val transparentInstancers = new mutable.HashMap[Mesh3D, Instancer]() + private val transparentNodes = new mutable.ArrayBuffer[SceneMesh3D]() + + def draw(scene: Scene3D): Unit = { + setupCamera(scene) + setupLight(scene) + collectOpaque(scene) + collectTransparent(scene) + sortOpaque(scene) + sortTransparent(scene) + + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer) + GL11.glViewport(0, 0, width, height) + GL11.glDisable(GL11.GL_SCISSOR_TEST) + GL11.glEnable(GL11.GL_DEPTH_TEST) + GL11.glEnable(GL11.GL_CULL_FACE) + + GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f) + GL11.glClearDepth(1.0f) + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT) + + Spritesheet.texture.bind() + drawOpaque() + drawTransparent() + } + + private def setupCamera(scene: Scene3D): Unit = { + shaderProgram.set("uView", scene.camera.transform.inverse) + shaderProgram.set("uProj", scene.camera.projectionMatrix(width.toFloat / height.toFloat)) + } + + private def setupLight(scene: Scene3D): Unit = { + shaderProgram.set("uLightDir", scene.lightSource.basis.up) + shaderProgram.set("uLightColor", scene.lightSource.color.toRGBANorm.rgbVector * scene.lightSource.energy) + + shaderProgram.set("uAmbientLightColor", scene.ambientLightColor.toRGBANorm.rgbVector * scene.ambientLightEnergy) + } + + private def collectOpaque(scene: Scene3D): Unit = { + for (node <- scene.meshes.iterator.filter(_.isOpaque)) { + // TODO: frustum culling + val instancer = getOpaqueInstancer(node.mesh) + instancer.schedule(nodeToInstance(node)) + } + } + + private def collectTransparent(scene: Scene3D): Unit = { + transparentNodes.clear() + for (node <- scene.meshes.iterator.filter(!_.isOpaque)) { + transparentNodes.append(node) + } + } + + private def sortOpaque(scene: Scene3D): Unit = { + for ((mesh, instancer) <- opaqueInstancers) { + val meshCenter = mesh.boundingBox.center + instancer.sort((a, b) => { + val aDist = scene.camera.distanceTo(a.transform * meshCenter) + val bDist = scene.camera.distanceTo(b.transform * meshCenter) + aDist < bDist // closest first + }) + } + } + + private def sortTransparent(scene: Scene3D): Unit = { + transparentNodes.sortWith((a, b) => { + val aDist = scene.camera.distanceTo(a.transform * a.mesh.boundingBox.center) + val bDist = scene.camera.distanceTo(b.transform * b.mesh.boundingBox.center) + a.priority < b.priority || aDist > bDist // farthest first + }) + } + + private def drawOpaque(): Unit = { + opaqueInstancers.values.foreach(_.flush()) + } + + private def drawTransparent(): Unit = { + var i = 0 + while (i < transparentNodes.length) { + val node = transparentNodes(i) + i += 1 + + val instancer = getTransparentInstancer(node.mesh) + instancer.schedule(nodeToInstance(node)) + + while (i < transparentNodes.length && transparentNodes(i).mesh == node.mesh) { + val similarNode = transparentNodes(i) + instancer.schedule(nodeToInstance(similarNode)) + i += 1 + } + + instancer.flush() + } + } + + private def getOpaqueInstancer(mesh: Mesh3D): Instancer = { + opaqueInstancers.getOrElseUpdate(mesh, new Instancer(mesh, MeshInstance3D, shaderProgram)) + } + + private def getTransparentInstancer(mesh: Mesh3D): Instancer = { + transparentInstancers.getOrElseUpdate(mesh, new Instancer(mesh, MeshInstance3D, shaderProgram)) + } + + private def nodeToInstance(node: SceneMesh3D): MeshInstance3D = { + MeshInstance3D(node.color, node.transform, node.uvTransform) + } } diff --git a/src/main/scala/ocelot/desktop/graphics/mesh/Mesh3D.scala b/src/main/scala/ocelot/desktop/graphics/mesh/Mesh3D.scala index 776e57c..876564c 100644 --- a/src/main/scala/ocelot/desktop/graphics/mesh/Mesh3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/mesh/Mesh3D.scala @@ -1,5 +1,68 @@ package ocelot.desktop.graphics.mesh +import ocelot.desktop.geometry.{Box3D, Vector2D, Vector3D} import ocelot.desktop.graphics.buffer.Index -case class Mesh3D(vertices: Seq[MeshVertex3D], indices: Option[Seq[Index]] = None) extends Mesh[MeshVertex3D] +object Mesh3D { + val Cube: Mesh3D = { + val builder = new MeshBuilder3D + + // TODO: uv coords + + // left + builder.quad( + Vector3D(-1.0, -1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, -1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, +1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, +1.0, -1.0), Vector2D(0.0, 0.0), + ) + + // right + builder.quad( + Vector3D(+1.0, -1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, +1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, +1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, -1.0, +1.0), Vector2D(0.0, 0.0), + ) + + // front + builder.quad( + Vector3D(-1.0, -1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, +1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, +1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, -1.0, -1.0), Vector2D(0.0, 0.0), + ) + + // back + builder.quad( + Vector3D(-1.0, -1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, -1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, +1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, +1.0, +1.0), Vector2D(0.0, 0.0), + ) + + // up + builder.quad( + Vector3D(-1.0, +1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, +1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, +1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, +1.0, -1.0), Vector2D(0.0, 0.0), + ) + + // down + builder.quad( + Vector3D(-1.0, -1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, -1.0, -1.0), Vector2D(0.0, 0.0), + Vector3D(+1.0, -1.0, +1.0), Vector2D(0.0, 0.0), + Vector3D(-1.0, -1.0, +1.0), Vector2D(0.0, 0.0), + ) + + builder.build() + } +} + +case class Mesh3D(vertices: Seq[MeshVertex3D], indices: Option[Seq[Index]] = None) extends Mesh[MeshVertex3D] { + val boundingBox: Box3D = { + Box3D.fromVertices(vertices.iterator.map(_.pos)) + } +} diff --git a/src/main/scala/ocelot/desktop/graphics/mesh/MeshBuilder3D.scala b/src/main/scala/ocelot/desktop/graphics/mesh/MeshBuilder3D.scala new file mode 100644 index 0000000..ec7a9c5 --- /dev/null +++ b/src/main/scala/ocelot/desktop/graphics/mesh/MeshBuilder3D.scala @@ -0,0 +1,51 @@ +package ocelot.desktop.graphics.mesh + +import ocelot.desktop.geometry.{Vector2D, Vector3D} +import ocelot.desktop.graphics.buffer.Index + +import scala.collection.mutable.ArrayBuffer + +class MeshBuilder3D { + private val vertices = new ArrayBuffer[MeshVertex3D]() + private val indices = new ArrayBuffer[Index]() + + def vertex(pos: Vector3D, normal: Vector3D, uv: Vector2D): Index = { + vertices += MeshVertex3D(pos, normal, uv) + Index(vertices.length - 1) + } + + def triangleIndices(a: Index, b: Index, c: Index): Unit = { + indices += a + indices += b + indices += c + } + + def triangle(a: Vector3D, aUV: Vector2D, + b: Vector3D, bUV: Vector2D, + c: Vector3D, cUV: Vector2D): Unit = + { + val normal = (b - a).cross(c - a).normalize + val aIdx = vertex(a, normal, aUV) + val bIdx = vertex(b, normal, bUV) + val cIdx = vertex(c, normal, cUV) + triangleIndices(aIdx, bIdx, cIdx) + } + + def quad(a: Vector3D, aUV: Vector2D, + b: Vector3D, bUV: Vector2D, + c: Vector3D, cUV: Vector2D, + d: Vector3D, dUV: Vector2D): Unit = + { + val normal = (b - a).cross(c - a).normalize + val aIdx = vertex(a, normal, aUV) + val bIdx = vertex(b, normal, bUV) + val cIdx = vertex(c, normal, cUV) + val dIdx = vertex(d, normal, dUV) + triangleIndices(aIdx, bIdx, cIdx) + triangleIndices(aIdx, cIdx, dIdx) + } + + def build(): Mesh3D = { + Mesh3D(vertices.toSeq, indices = Some(indices.toSeq)) + } +} diff --git a/src/main/scala/ocelot/desktop/graphics/mesh/MeshInstance3D.scala b/src/main/scala/ocelot/desktop/graphics/mesh/MeshInstance3D.scala index 5df533b..aff4ad9 100644 --- a/src/main/scala/ocelot/desktop/graphics/mesh/MeshInstance3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/mesh/MeshInstance3D.scala @@ -19,7 +19,7 @@ object MeshInstance3D extends VertexType { ) } -class MeshInstance3D(color: Color, transform: Transform3D, uvTransform: Transform2D) extends Vertex { +case class MeshInstance3D(color: Color, transform: Transform3D, uvTransform: Transform2D) extends Vertex { override def vertexType: VertexType = MeshInstance3D override def stride: Int = MeshInstance3D.stride diff --git a/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala b/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala index 6410e68..87c9c35 100644 --- a/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala +++ b/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala @@ -2,7 +2,7 @@ package ocelot.desktop.graphics.render import ocelot.desktop.graphics.ShaderProgram import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer} -import ocelot.desktop.graphics.mesh.{Mesh, MeshInstance2D, Vertex, VertexArray} +import ocelot.desktop.graphics.mesh.{Mesh, Vertex, VertexArray, VertexType} import ocelot.desktop.util.{Logging, Resource} import org.lwjgl.BufferUtils import org.lwjgl.opengl._ @@ -10,12 +10,12 @@ import org.lwjgl.opengl._ import java.nio.ByteBuffer import scala.collection.mutable.ArrayBuffer -class InstanceRenderer[V <: Vertex, I <: Vertex](mesh: Mesh[V], shader: ShaderProgram) extends Resource with Logging { +class InstanceRenderer[V <: Vertex, I <: Vertex](mesh: Mesh[V], instanceType: VertexType, shader: ShaderProgram) extends Resource with Logging { private val InitialCapacity: Int = 4096 private val vertexBuffer = new VertexBuffer[V](mesh.vertices) private val indexBuffer: Option[IndexBuffer] = mesh.indices.map(indices => new IndexBuffer(indices)) - private val instanceBuffer = new VertexBuffer[I](MeshInstance2D, InitialCapacity) + private val instanceBuffer = new VertexBuffer[I](instanceType, InitialCapacity) private val vertexArray = new VertexArray(shader) @@ -29,6 +29,10 @@ class InstanceRenderer[V <: Vertex, I <: Vertex](mesh: Mesh[V], shader: ShaderPr instances += instance } + def sort(ltComparator: (I, I) => Boolean): Unit = { + instances.sortInPlaceWith(ltComparator) + } + def flush(): Unit = { write() draw() @@ -48,10 +52,10 @@ class InstanceRenderer[V <: Vertex, I <: Vertex](mesh: Mesh[V], shader: ShaderPr indexBuffer match { case Some(ib) => - val method = classOf[ARBDrawInstanced].getDeclaredMethod("nglDrawElementsInstancedARBBO", - Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, java.lang.Long.TYPE) + val method = classOf[ARBDrawInstanced].getDeclaredMethod("nglDrawElementsInstancedARB", + Integer.TYPE, Integer.TYPE, Integer.TYPE, java.lang.Long.TYPE, Integer.TYPE, java.lang.Long.TYPE) method.setAccessible(true) - method.invoke(null, GL11.GL_TRIANGLES, ib.capacity, GL11.GL_UNSIGNED_INT, 0, instances.length, glDrawElementsInstancedARBptr) + method.invoke(null, GL11.GL_TRIANGLES, ib.capacity, GL11.GL_UNSIGNED_INT, 0L, instances.length, glDrawElementsInstancedARBptr) case None => val method = classOf[ARBDrawInstanced].getDeclaredMethod("nglDrawArraysInstancedARB", Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, java.lang.Long.TYPE) diff --git a/src/main/scala/ocelot/desktop/graphics/scene/Camera3D.scala b/src/main/scala/ocelot/desktop/graphics/scene/Camera3D.scala index 05e0b7e..4291ce8 100644 --- a/src/main/scala/ocelot/desktop/graphics/scene/Camera3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/scene/Camera3D.scala @@ -1,10 +1,14 @@ package ocelot.desktop.graphics.scene -import ocelot.desktop.geometry.ProjectionMatrix3D +import ocelot.desktop.geometry.{ProjectionMatrix3D, Vector3D} class Camera3D(var zNear: Float = 0.01f, var zFar: Float = 1000.0f, var fovY: Float = 80.0f) extends SceneNode3D { + def distanceTo(point: Vector3D): Float = { + (origin - point).length + } + def projectionMatrix(aspect: Float): ProjectionMatrix3D = ProjectionMatrix3D.perspective(aspect, fovY, zNear, zFar) } diff --git a/src/main/scala/ocelot/desktop/graphics/scene/DirectionalLight3D.scala b/src/main/scala/ocelot/desktop/graphics/scene/DirectionalLight3D.scala index 6efe3c5..f30a058 100644 --- a/src/main/scala/ocelot/desktop/graphics/scene/DirectionalLight3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/scene/DirectionalLight3D.scala @@ -3,4 +3,4 @@ package ocelot.desktop.graphics.scene import ocelot.desktop.color.Color class DirectionalLight3D(var color: Color = Color.White, - var strength: Float = 1) extends SceneNode3D + var energy: Float = 1) extends SceneNode3D diff --git a/src/main/scala/ocelot/desktop/graphics/scene/Scene3D.scala b/src/main/scala/ocelot/desktop/graphics/scene/Scene3D.scala index ed5ea5a..3e1c435 100644 --- a/src/main/scala/ocelot/desktop/graphics/scene/Scene3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/scene/Scene3D.scala @@ -1,10 +1,13 @@ package ocelot.desktop.graphics.scene +import ocelot.desktop.color.Color + import scala.collection.mutable.ArrayBuffer class Scene3D { var camera: Camera3D = new Camera3D var lightSource: DirectionalLight3D = new DirectionalLight3D - var ambientLight: Float = 0.1f + var ambientLightEnergy: Float = 0.3f + var ambientLightColor: Color = Color.White val meshes: ArrayBuffer[SceneMesh3D] = new ArrayBuffer[SceneMesh3D]() } diff --git a/src/main/scala/ocelot/desktop/graphics/scene/SceneMesh3D.scala b/src/main/scala/ocelot/desktop/graphics/scene/SceneMesh3D.scala index 92c7f3a..4746628 100644 --- a/src/main/scala/ocelot/desktop/graphics/scene/SceneMesh3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/scene/SceneMesh3D.scala @@ -1,8 +1,11 @@ package ocelot.desktop.graphics.scene import ocelot.desktop.color.Color +import ocelot.desktop.geometry.Transform2D import ocelot.desktop.graphics.mesh.Mesh3D class SceneMesh3D(var mesh: Mesh3D, var color: Color = Color.White, - var isOpaque: Boolean = true) extends SceneNode3D \ No newline at end of file + var uvTransform: Transform2D = Transform2D.identity, + var isOpaque: Boolean = true, + var priority: Int = 0) extends SceneNode3D \ No newline at end of file diff --git a/src/main/scala/ocelot/desktop/graphics/scene/SceneNode3D.scala b/src/main/scala/ocelot/desktop/graphics/scene/SceneNode3D.scala index 7809874..b8825e1 100644 --- a/src/main/scala/ocelot/desktop/graphics/scene/SceneNode3D.scala +++ b/src/main/scala/ocelot/desktop/graphics/scene/SceneNode3D.scala @@ -1,7 +1,15 @@ package ocelot.desktop.graphics.scene -import ocelot.desktop.geometry.Transform3D +import ocelot.desktop.geometry.{Basis3D, Transform3D, Vector3D} trait SceneNode3D { var transform: Transform3D = Transform3D.identity + + final def origin: Vector3D = transform.origin + + final def origin_=(new_origin: Vector3D): Unit = transform = Transform3D(basis, new_origin) + + final def basis: Basis3D = transform.basis + + final def basis_=(new_basis: Basis3D): Unit = transform = Transform3D(new_basis, origin) } diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index b0f0abd..e1c7b9d 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -195,7 +195,7 @@ object UiHandler extends Logging { logger.info("VSync disabled (via config)") } - Display.create() + Display.create(new PixelFormat, new ContextAttribs(3, 2)) if (Settings.get.windowValidatePosition) { fixInsaneInitialWindowGeometry() @@ -421,7 +421,7 @@ object UiHandler extends Logging { } private def draw(): Unit = { - graphics.startViewport() + graphics.begin() graphics.clear() root.draw(graphics) diff --git a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala index 8f07609..98450a2 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala @@ -11,11 +11,12 @@ import ocelot.desktop.ui.layout.{CopyLayout, Layout} import ocelot.desktop.ui.widget.window.{NodeSelector, ProfilerWindow, WindowPool} import ocelot.desktop.util.DrawUtils import ocelot.desktop.util.animation.ValueAnimation +import ocelot.desktop.windows.Viewport3DWindow import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings} import org.lwjgl.input.Keyboard import totoro.ocelot.brain.entity.traits.{Entity, Environment, SidedEnvironment} import totoro.ocelot.brain.entity.{Case, Screen} -import totoro.ocelot.brain.event.{EventBus, InventoryEntityAddedEvent, InventoryEvent, NodeEvent} +import totoro.ocelot.brain.event.{EventBus, InventoryEvent, NodeEvent} import totoro.ocelot.brain.nbt.ExtendedNBT._ import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound} import totoro.ocelot.brain.util.{Direction, Tier} @@ -30,6 +31,7 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover var windowPool = new WindowPool var nodeSelector = new NodeSelector var profilerWindow = new ProfilerWindow + val viewport3DWindow = new Viewport3DWindow var cameraOffset: Vector2D = Vector2D(0, 0) var newConnection: Option[(Node, NodePort, Vector2D)] = None @@ -205,6 +207,9 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F4, _) => windowPool.openWindow(profilerWindow) + + case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F8, _) => + windowPool.openWindow(viewport3DWindow) } private def openSelector(p: Vector2D): Unit = { diff --git a/src/main/scala/ocelot/desktop/windows/Viewport3DWindow.scala b/src/main/scala/ocelot/desktop/windows/Viewport3DWindow.scala new file mode 100644 index 0000000..65276ad --- /dev/null +++ b/src/main/scala/ocelot/desktop/windows/Viewport3DWindow.scala @@ -0,0 +1,68 @@ +package ocelot.desktop.windows + +import ocelot.desktop.geometry._ +import ocelot.desktop.graphics.mesh.Mesh3D +import ocelot.desktop.graphics.scene.{Scene3D, SceneMesh3D} +import ocelot.desktop.graphics.{Graphics, Viewport3D} +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.layout.{Layout, LinearLayout} +import ocelot.desktop.ui.widget.window.BasicWindow +import ocelot.desktop.ui.widget.{Label, PaddingBox, Widget} +import ocelot.desktop.util.{DrawUtils, Orientation} + +class Viewport3DWindow extends BasicWindow { + private lazy val viewport = new Viewport3D(600, 400) + + children :+= new PaddingBox(new Widget { + override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) + + children :+= new PaddingBox(new Label { + override def text: String = "3D Viewport" + + override val isSmall: Boolean = true + }, Padding2D.equal(4)) + + children :+= new PaddingBox(new Widget { + override def minimumSize: Size2D = Size2D(600, 400) + + override def maximumSize: Size2D = Size2D(600, 400) + + override def draw(g: Graphics): Unit = { + g.blitViewport3D(viewport, bounds) + } + }, Padding2D(left = 2, right = 2, bottom = 2)) + }, Padding2D.equal(8)) + + override def draw(g: Graphics): Unit = { + viewport.draw(createScene()) + + beginDraw(g) + DrawUtils.windowWithShadow(g, position.x, position.y, size.width, size.height, 1f, 0.5f) + drawChildren(g) + endDraw(g) + } + + private var time = 0.0f + + private def createScene(): Scene3D = { + val scene = new Scene3D + + scene.camera.transform = Transform3D.lookingAt(Vector3D(math.cos(time).toFloat * 10f, 5, math.sin(time).toFloat * 10f), Vector3D.Zero) + scene.lightSource.basis = Basis3D.rotate(Vector3D.AxisZ, 30f.toRadians) * Basis3D.rotate(Vector3D.AxisX, 30f.toRadians) + + for (sx <- -10 to 10) { + for (sy <- -10 to 10) { + scene.meshes += { + val mesh = new SceneMesh3D(Mesh3D.Cube) + mesh.origin = Vector3D(sx * 3, 0.0, sy * 3) + mesh + } + + } + } + + time += UiHandler.dt + + scene + } +}