Last active
September 3, 2024 02:35
-
-
Save LucasAlfare/03677ecd483a8c6a410b2ead4de12b8e to your computer and use it in GitHub Desktop.
Studying 3D render using easy to ready Kotlin implementations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:Suppress("unused", "MemberVisibilityCanBePrivate") | |
package study.math | |
import kotlin.math.cos | |
import kotlin.math.sin | |
/** | |
* Represents a 4x4 matrix used for various transformations in 3D space. | |
* | |
* This class treats a linear collection of elements as a bidimensional 4x4 matrix, | |
* providing methods to perform common matrix operations such as translation, | |
* scaling, and rotation along the X, Y, and Z axes. It also supports matrix | |
* multiplication and provides utility functions for setting and retrieving matrix elements. | |
* | |
* @property elements An array of 16 double values representing the matrix elements in row-major order. | |
* | |
* @throws IllegalArgumentException If the number of elements provided is not exactly 16. | |
*/ | |
class Matrix4x4(vararg var elements: Double) { | |
/** | |
* Initializes the Matrix4x4 instance. | |
* | |
* If exactly 16 elements are provided, they are used to populate the matrix. | |
* If no elements are provided, the matrix is initialized as an identity matrix. | |
* | |
* @throws IllegalArgumentException If the number of elements is between 1 and 15. | |
*/ | |
init { | |
// Only creates a matrix if it has exactly 16 elements or... | |
if (elements.size in 1..15) | |
throw IllegalArgumentException("Matrix must contain exactly 16 elements.") | |
// ...if it is empty (then this is set to identity matrix) | |
if (elements.isEmpty()) { | |
elements = DoubleArray(16) { 0.0 } | |
setIdentity() | |
} | |
} | |
/** | |
* Companion object containing factory methods for creating common transformation matrices. | |
*/ | |
companion object { | |
/** | |
* Creates a translation matrix based on the provided translation values. | |
* | |
* @param tx Translation along the X-axis. Defaults to 0.0. | |
* @param ty Translation along the Y-axis. Defaults to 0.0. | |
* @param tz Translation along the Z-axis. Defaults to 0.0. | |
* @return A new Matrix4x4 instance representing the translation. | |
* | |
* @see [Translation Matrix](https://static.javatpoint.com/tutorial/computer-graphics/images/computer-graphics-3d-transformations3.png) | |
*/ | |
fun translationMatrix(tx: Double = 0.0, ty: Double = 0.0, tz: Double = 0.0): Matrix4x4 { | |
// @formatter:off | |
return Matrix4x4( | |
1.0, 0.0, 0.0, tx, | |
0.0, 1.0, 0.0, ty, | |
0.0, 0.0, 1.0, tz, | |
0.0, 0.0, 0.0, 1.0 | |
) | |
// @formatter:on | |
} | |
/** | |
* Creates a scaling matrix based on the provided scaling factors. | |
* | |
* @param sx Scaling factor along the X-axis. Defaults to 1.0. | |
* @param sy Scaling factor along the Y-axis. Defaults to 1.0. | |
* @param sz Scaling factor along the Z-axis. Defaults to 1.0. | |
* @return A new Matrix4x4 instance representing the scaling. | |
* | |
* @see [Scaling Matrix](http://www.c-jump.com/bcc/common/Talk3/Math/Matrices/const_images/applying_scaling.png) | |
*/ | |
fun scaleMatrix(sx: Double = 1.0, sy: Double = 1.0, sz: Double = 1.0): Matrix4x4 { | |
// @formatter:off | |
return Matrix4x4( | |
sx, 0.0, 0.0, 0.0, | |
0.0, sy, 0.0, 0.0, | |
0.0, 0.0, sz, 0.0, | |
0.0, 0.0, 0.0, 1.0 | |
) | |
// @formatter:on | |
} | |
/** | |
* Creates a rotation matrix around the X-axis based on the provided angle. | |
* | |
* @param degreeAngle The angle in degrees to rotate around the X-axis. Defaults to 0.0. | |
* @return A new Matrix4x4 instance representing the rotation around the X-axis. | |
* | |
* @see [Rotation Matrix X](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQkOm2Vs8YxvEDDMNHS8vRNrdfavXSfcM4JuA&s) | |
*/ | |
fun rotationXMatrix(degreeAngle: Double = 0.0): Matrix4x4 { | |
val radians = Math.toRadians(degreeAngle) | |
val sin = sin(radians) | |
val cos = cos(radians) | |
// @formatter:off | |
return Matrix4x4( | |
1.0, 0.0, 0.0, 0.0, | |
0.0, cos, -sin, 0.0, | |
0.0, sin, cos, 0.0, | |
0.0, 0.0, 0.0, 1.0 | |
) | |
// @formatter:on | |
} | |
/** | |
* Creates a rotation matrix around the Y-axis based on the provided angle. | |
* | |
* @param degreeAngle The angle in degrees to rotate around the Y-axis. Defaults to 0.0. | |
* @return A new Matrix4x4 instance representing the rotation around the Y-axis. | |
* | |
* @see [Rotation Matrix Y](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQkOm2Vs8YxvEDDMNHS8vRNrdfavXSfcM4JuA&s) | |
*/ | |
fun rotationYMatrix(degreeAngle: Double = 0.0): Matrix4x4 { | |
val radians = Math.toRadians(degreeAngle) | |
val sin = sin(radians) | |
val cos = cos(radians) | |
// @formatter:off | |
return Matrix4x4( | |
cos, 0.0, sin, 0.0, | |
0.0, 1.0, 0.0, 0.0, | |
-sin, 0.0, cos, 0.0, | |
0.0, 0.0, 0.0, 1.0 | |
) | |
// @formatter:on | |
} | |
/** | |
* Creates a rotation matrix around the Z-axis based on the provided angle. | |
* | |
* @param degreeAngle The angle in degrees to rotate around the Z-axis. Defaults to 0.0. | |
* @return A new Matrix4x4 instance representing the rotation around the Z-axis. | |
* | |
* @see [Rotation Matrix Z](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQkOm2Vs8YxvEDDMNHS8vRNrdfavXSfcM4JuA&s) | |
*/ | |
fun rotationZMatrix(degreeAngle: Double = 0.0): Matrix4x4 { | |
val radians = Math.toRadians(degreeAngle) | |
val sin = sin(radians) | |
val cos = cos(radians) | |
// @formatter:off | |
return Matrix4x4( | |
cos, -sin, 0.0, 0.0, | |
sin, cos, 0.0, 0.0, | |
0.0, 0.0, 1.0, 0.0, | |
0.0, 0.0, 0.0, 1.0 | |
) | |
// @formatter:on | |
} | |
} | |
/** | |
* Multiplies the current matrix with another Matrix4x4. | |
* | |
* This method performs standard matrix multiplication, where each element of the resulting | |
* matrix is the dot product of the corresponding row from the first matrix and the column | |
* from the second matrix. | |
* | |
* @param m The Matrix4x4 to multiply with the current matrix. | |
* @return A new Matrix4x4 instance representing the product of the two matrices. | |
*/ | |
fun multiply(m: Matrix4x4): Matrix4x4 { | |
val result = Matrix4x4() | |
repeat(4) { i -> | |
repeat(4) { j -> | |
var sum = 0.0 | |
repeat(4) { k -> | |
sum += this.get(k, i) * m.get(j, k) | |
} | |
result.set(j, i, sum) | |
} | |
} | |
return result | |
} | |
/** | |
* Sets the current matrix to be an identity matrix. | |
* | |
* This method sets the diagonal elements (top-left to bottom-right) to 1.0 and all other | |
* elements to 0.0, effectively making it an identity matrix. | |
* | |
* @return The current Matrix4x4 instance after being set to identity. | |
*/ | |
fun setIdentity(): Matrix4x4 { | |
repeat(4) { y -> | |
repeat(4) { x -> | |
if (x == y) set(x, y, 1.0) | |
else set(x, y, 0.0) | |
} | |
} | |
return this | |
} | |
/** | |
* Retrieves the value at the specified coordinates in the matrix. | |
* | |
* @param x The column index (0-based). | |
* @param y The row index (0-based). | |
* @return The double value at the specified (x, y) position. | |
* | |
* @throws IndexOutOfBoundsException If x or y is outside the range [0, 3]. | |
*/ | |
fun get(x: Int, y: Int): Double { | |
return elements[x + y * 4] | |
} | |
/** | |
* Sets the value at the specified coordinates in the matrix. | |
* | |
* @param x The column index (0-based). | |
* @param y The row index (0-based). | |
* @param d The double value to set at the specified (x, y) position. | |
* | |
* @throws IndexOutOfBoundsException If x or y is outside the range [0, 3]. | |
*/ | |
fun set(x: Int, y: Int, d: Double) { | |
elements[x + y * 4] = d | |
} | |
/** | |
* Returns a string representation of the matrix for debugging purposes. | |
* | |
* The matrix is formatted in a readable 4x4 structure with each element rounded to one decimal place. | |
* | |
* @return A string representing the matrix. | |
*/ | |
override fun toString(): String { | |
var s = "[\n" | |
repeat(4) { y -> | |
repeat(4) { x -> | |
s += "\t" | |
s += "${String.format("%.1f", get(x, y))} ".replace(",", ".") | |
} | |
s += "\n" | |
} | |
return "$s]" | |
} | |
/** | |
* Checks if this matrix is equal to another object. | |
* | |
* Two Matrix4x4 instances are considered equal if their elements are identical. | |
* | |
* @param other The object to compare with. | |
* @return `true` if the other object is a Matrix4x4 with the same elements, `false` otherwise. | |
*/ | |
override fun equals(other: Any?): Boolean { | |
if (other == null) return false | |
if (other !is Matrix4x4) return false | |
return this.elements.contentEquals(other.elements) | |
} | |
/** | |
* Returns the hash code for this matrix. | |
* | |
* This implementation is based on the hash code of the elements array. | |
* | |
* @return The hash code of the matrix. | |
*/ | |
override fun hashCode(): Int { | |
return elements.contentHashCode() | |
} | |
operator fun times(m: Matrix4x4) = multiply(m) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:Suppress("MemberVisibilityCanBePrivate") | |
package study.math | |
import kotlin.math.pow | |
import kotlin.math.sqrt | |
/** | |
* Represents a 4-dimensional vector, commonly used in 3D graphics for homogeneous coordinates. | |
* | |
* @property x The x-coordinate of the vector. | |
* @property y The y-coordinate of the vector. | |
* @property z The z-coordinate of the vector. | |
* @property w The w-coordinate (homogeneous coordinate) of the vector, with a default value of 1.0. | |
*/ | |
data class Vector4( | |
var x: Double = 0.0, | |
var y: Double = 0.0, | |
var z: Double = 0.0, | |
var w: Double = 1.0 | |
) { | |
/** | |
* Adds this vector to another vector [v] and returns a new [Vector4] representing the result. | |
* | |
* @param v The vector to be added. | |
* @return A new [Vector4] representing the sum of the two vectors. | |
*/ | |
fun add(v: Vector4) = Vector4(x + v.x, y + v.y, z + v.z, w + v.w) | |
/** | |
* Subtracts another vector [v] from this vector and returns a new [Vector4] representing the result. | |
* | |
* @param v The vector to be subtracted. | |
* @return A new [Vector4] representing the difference between the two vectors. | |
*/ | |
fun subtract(v: Vector4) = Vector4(x - v.x, y - v.y, z - v.z, w - v.w) | |
/** | |
* Scales this vector by a scalar value and returns a new [Vector4] representing the result. | |
* | |
* @param scalar The scalar value by which to scale the vector. | |
* @return A new [Vector4] representing the scaled vector. | |
*/ | |
fun scale(scalar: Double) = Vector4(x * scalar, y * scalar, z * scalar, w * scalar) | |
/** | |
* Computes the dot product between this vector and another vector [v]. | |
* | |
* The dot product is a measure of how much one vector extends in the direction of another vector. | |
* | |
* @param v The vector to compute the dot product with. | |
* @return The dot product as a [Double]. | |
*/ | |
fun dotProduct(v: Vector4): Double = (x * v.x) + (y * v.y) + (z * v.z) | |
/** | |
* Computes the cross product between this vector and another vector [v] and returns a new [Vector4] representing the result. | |
* | |
* The cross product is a vector that is perpendicular to both original vectors and has a magnitude | |
* equal to the area of the parallelogram that the vectors span. | |
* | |
* Note: The w component is ignored in this calculation, as cross product is typically used in 3D space. | |
* | |
* @param v The vector to compute the cross product with. | |
* @return A new [Vector4] representing the cross product. | |
*/ | |
fun crossProduct(v: Vector4): Vector4 = Vector4( | |
x = (y * v.z) - (z * v.y), | |
y = (z * v.x) - (x * v.z), | |
z = (x * v.y) - (y * v.x), | |
w = 0.0 // The w component is set to 0 for cross products. | |
) | |
/** | |
* Multiplies this vector by a [Matrix4x4] and returns a new [Vector4] representing the result. | |
* | |
* This operation is typically used to transform the vector by applying translation, rotation, scale, | |
* or other transformations encapsulated by the matrix. | |
* | |
* @param m The matrix by which to multiply this vector. | |
* @return A new [Vector4] representing the transformed vector. | |
*/ | |
fun multiply(m: Matrix4x4): Vector4 = Vector4( | |
x = (m.get(0, 0) * x) + (m.get(1, 0) * y) + (m.get(2, 0) * z) + (m.get(3, 0) * w), | |
y = (m.get(0, 1) * x) + (m.get(1, 1) * y) + (m.get(2, 1) * z) + (m.get(3, 1) * w), | |
z = (m.get(0, 2) * x) + (m.get(1, 2) * y) + (m.get(2, 2) * z) + (m.get(3, 2) * w), | |
w = (m.get(0, 3) * x) + (m.get(1, 3) * y) + (m.get(2, 3) * z) + (m.get(3, 3) * w) | |
) | |
/** | |
* Computes the Euclidean length (magnitude) of this vector. | |
* | |
* The magnitude is a measure of the vector's length in 3D space. | |
* | |
* @return The magnitude as a [Double]. | |
*/ | |
fun magnitude() = sqrt(x.pow(2.0) + y.pow(2.0) + z.pow(2.0)) | |
/** | |
* Normalizes this vector, producing a unit vector in the same direction. | |
* | |
* A unit vector has a magnitude of 1. If the magnitude is 0 or 1, the original vector is returned unchanged. | |
* | |
* @return A new [Vector4] representing the normalized vector. | |
*/ | |
fun normalized(): Vector4 { | |
val magnitude = magnitude() | |
return if (magnitude == 0.0 || magnitude == 1.0) this else Vector4(x / magnitude, y / magnitude, z / magnitude, w) | |
} | |
/** | |
* Normalizes this vector with respect to its w component, dividing x, y, and z by w. | |
* | |
* This operation is commonly used in graphics pipelines to project points from 4D homogeneous coordinates | |
* back to 3D space. If w is 0, the original vector is returned unchanged. | |
* | |
* @return A new [Vector4] representing the w-normalized vector. | |
*/ | |
fun wNormalized(): Vector4 { | |
return if (w == 0.0) this else Vector4(x / w, y / w, z / w, 1.0) | |
} | |
/** | |
* Provides a string representation of the vector, primarily for debugging purposes. | |
* | |
* @return A [String] in the format "x, y, z, w". | |
*/ | |
override fun toString() = "$x, $y, $z, $w" | |
// operators functions | |
operator fun plus(v: Vector4) = add(v) | |
operator fun times(m: Matrix4x4) = multiply(m) | |
operator fun minus(v: Vector4) = subtract(v) | |
// inline function to call cross product | |
infix fun x(v: Vector4) = crossProduct(v) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package study.graphics | |
import study.math.Matrix4x4 | |
import study.math.Vector4 | |
import kotlin.math.tan | |
/** | |
* This class represents a camera in a 3D space, providing methods to generate perspective and view matrices. | |
* The camera operates in a coordinate system where: | |
* | |
* ``` | |
* O--------------- x | |
* | | |
* | | |
* | | |
* | | |
* y | |
* ``` | |
* | |
* The "Z" dimension extends inward (into the screen) for positive values, meaning that objects further away | |
* have higher Z values. | |
* | |
* @property aspectRatio The ratio of the camera's width to its height. This affects the perspective projection. | |
* @property fov Field of view angle in degrees, defining the extent of the observable world seen by the camera. | |
* @property near The distance to the near clipping plane, below which objects are not rendered. | |
* @property far The distance to the far clipping plane, beyond which objects are not rendered. | |
* @property position The position of the camera in 3D space, often referred to as the "eye" position. | |
* @property target The point in 3D space the camera is looking at, used to define the direction of view. | |
* @property up The "up" direction vector relative to the camera, used to maintain orientation. | |
*/ | |
data class Camera( | |
var aspectRatio: Double = 0.0, | |
var fov: Double = 0.0, // field of view angle in degrees | |
var near: Double = 1.0, | |
var far: Double = 100.0, | |
var position: Vector4 = Vector4(), // "eye" | |
var target: Vector4 = Vector4(0.0, 0.0, -1.0), // "looking at point" | |
val up: Vector4 = Vector4(0.0, 1.0, 0.0, 0.0) // "up direction" | |
) { | |
/** | |
* Companion object providing static functions to generate the perspective and view matrices for the camera. | |
*/ | |
companion object { | |
/** | |
* Generates a perspective projection matrix based on the camera's parameters. | |
* This matrix is used to simulate the effect of a 3D perspective, where objects further away appear smaller. | |
* | |
* @param camera The camera instance containing the parameters for the projection. | |
* @return A 4x4 matrix representing the perspective projection. | |
* | |
* The perspective matrix is calculated using the following formula: | |
* - The `fov` is converted from degrees to radians and halved to get `halfFov`. | |
* - `tanHalfFov` is the tangent of `halfFov`, which determines the scaling based on the field of view. | |
* - The diagonal elements of the matrix are scaled inversely by the aspect ratio and `tanHalfFov`. | |
* - The near and far planes are used to scale and translate the Z-values into normalized device coordinates. | |
*/ | |
fun perspectiveMatrix(camera: Camera): Matrix4x4 { | |
// fov refers to a "degree angle" | |
val halfFov = (camera.fov / 2.0) | |
val tanHalfFov = tan(Math.toRadians(halfFov)) | |
// @formatter:off | |
return Matrix4x4( | |
(1 / (camera.aspectRatio * tanHalfFov)), 0.0, 0.0, 0.0, | |
0.0, (1 / tanHalfFov), 0.0, 0.0, | |
0.0, 0.0, -((camera.far + camera.near) / (camera.far - camera.near)), -((2 * camera.far * camera.near) / (camera.far - camera.near)), | |
0.0, 0.0, -1.0, 0.0 | |
) | |
// @formatter:on | |
} | |
/** | |
* Generates a view matrix for the camera, which transforms world coordinates to camera coordinates. | |
* This matrix is responsible for the camera's orientation and position in the world space. | |
* | |
* @param camera The camera instance for which to generate the view matrix. | |
* @return A 4x4 matrix representing the view transformation. | |
* | |
* The view matrix is constructed as follows: | |
* - The `zAxis` is the normalized vector from the camera's position to its target, representing the viewing direction. | |
* - The `xAxis` is computed as the cross product of the camera's `up` vector and the `zAxis`, representing the camera's right direction. | |
* - The `yAxis` is the cross product of the `zAxis` and `xAxis`, representing the camera's up direction. | |
* - The matrix is filled with the components of the `xAxis`, `yAxis`, and `zAxis`, and the camera's position is used to translate the world coordinates. | |
*/ | |
fun viewMatrix(camera: Camera): Matrix4x4 { | |
val zAxis = (camera.target - camera.position).normalized() | |
val xAxis = (camera.up x zAxis).normalized() | |
val yAxis = zAxis x xAxis | |
// precomputed orientation * (position (inverted)) matrixes | |
return Matrix4x4( | |
xAxis.x, yAxis.x, zAxis.x, -camera.position.x, | |
xAxis.y, yAxis.y, zAxis.y, -camera.position.y, | |
xAxis.z, yAxis.z, zAxis.z, -camera.position.z, | |
0.0, 0.0, 0.0, 1.0 | |
) | |
} | |
} | |
/** | |
* Combines the view and perspective matrices to produce a final transformation matrix. | |
* This matrix is used to transform world coordinates directly into normalized device coordinates | |
* for rendering from the camera's point of view. | |
* | |
* @return A 4x4 matrix that is the product of the view matrix and perspective matrix. | |
* | |
* The combined matrix is crucial in the rendering pipeline, as it allows objects in the world space | |
* to be projected onto the screen with the correct perspective and orientation. | |
*/ | |
fun combinedMatrix() = viewMatrix(this) * perspectiveMatrix(this) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Currently, comments was generated using ChatGPT