samples: add software 3D renderer in Kotlin

This commit is contained in:
Alexey Andreev 2023-11-13 22:52:43 +01:00
parent 932f33ae2c
commit 01cf27b3d8
31 changed files with 1893 additions and 1 deletions

View File

@ -15,7 +15,7 @@
*/
plugins {
kotlin("jvm") version "1.8.0"
kotlin("multiplatform") version "1.9.20"
war
id("org.teavm")
}
@ -28,3 +28,7 @@ teavm.js {
addedToWebApp = true
mainClass = "org.teavm.samples.kotlin.HelloKt"
}
kotlin {
jvm()
}

View File

@ -59,6 +59,7 @@ include("pi")
include("kotlin")
include("scala")
include("web-apis")
include("software3d")
gradle.allprojects {
apply<WarPlugin>()

View File

@ -0,0 +1,67 @@
import org.teavm.gradle.api.OptimizationLevel
import org.teavm.gradle.tasks.TeaVMTask
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
kotlin("multiplatform") version "1.9.20"
war
id("org.teavm")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
teavm.js {
addedToWebApp = true
mainClass = "org.teavm.samples.software3d.teavm.MainKt"
}
teavm.wasm {
addedToWebApp = true
mainClass = "org.teavm.samples.software3d.teavm.WasmWorkerKt"
optimization = OptimizationLevel.AGGRESSIVE
minHeapSize = 4
}
kotlin {
js {
browser {
}
binaries.executable()
}
jvm()
sourceSets.jvmMain.dependencies {
implementation(teavm.libs.jsoApis)
}
}
tasks.withType<TeaVMTask> {
classpath.from(kotlin.jvm().compilations["main"].output.classesDirs)
}
tasks.war {
dependsOn(tasks.named("jsBrowserDistribution"))
with(copySpec {
from(kotlin.js().binaries.executable().map { it.distribution.outputDirectory })
into("kjs")
})
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.geometry
import org.teavm.samples.software3d.scene.Vertex
class Face(val a: Vertex, val b: Vertex, val c: Vertex)

View File

@ -0,0 +1,64 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.geometry
interface GeneralMatrix {
val size: Int
fun get(row: Int, col: Int): Float
fun determinant(): Float {
return if (size == 2) {
get(0, 0) * get(1, 1) - get(1, 0) * get(0, 1)
} else {
(0 until size)
.map { j ->
val r = get(0, j) * exclude(0, j).determinant()
if (j % 2 == 0) r else -r
}
.sum()
}
}
fun exclude(excludeRow: Int, excludeCol: Int): GeneralMatrix {
val orig = this
return object : GeneralMatrix {
override val size: Int
get() = orig.size - 1
override fun get(row: Int, col: Int): Float {
return orig.get(if (row < excludeRow) row else row + 1, if (col < excludeCol) col else col + 1)
}
}
}
fun asString(): String {
val cells = (0 until size).map { j ->
(0 until size).map { i ->
get(i, j).toString()
}
}
val lengths = cells.map { column -> column.maxOf { it.length } }
val padCells = (cells zip lengths).map { (column, length) ->
column.map { it + " ".repeat(length - it.length) }
}
return (0 until size).joinToString("\n") { i ->
(0 until size).joinToString(" ") { j -> padCells[j][i] }
}
}
}

View File

@ -0,0 +1,215 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.geometry
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
class Matrix(
val m11: Float, val m12: Float, val m13: Float, val m14: Float,
val m21: Float, val m22: Float, val m23: Float, val m24: Float,
val m31: Float, val m32: Float, val m33: Float, val m34: Float,
val m41: Float, val m42: Float, val m43: Float, val m44: Float
) : GeneralMatrix {
override val size: Int
get() = 4
override fun get(row: Int, col: Int): Float {
val offset = row * 4 + col
return when (offset) {
0 -> m11
1 -> m12
2 -> m13
3 -> m14
4 -> m21
5 -> m22
6 -> m23
7 -> m24
8 -> m31
9 -> m32
10 -> m33
11 -> m34
12 -> m41
13 -> m42
14 -> m43
15 -> m44
else -> error("Wrong row/column")
}
}
fun applyTo(v: Vector, result: MutableVector) {
with(result) {
x = m11 * v.x + m12 * v.y + m13 * v.z + m14 * v.w
y = m21 * v.x + m22 * v.y + m23 * v.z + m24 * v.w
z = m31 * v.x + m32 * v.y + m33 * v.z + m34 * v.w
w = m41 * v.x + m42 * v.y + m43 * v.z + m44 * v.w
}
}
fun applyDirTo(v: Vector, result: MutableVector) {
with(result) {
x = m11 * v.x + m12 * v.y + m13 * v.z
y = m21 * v.x + m22 * v.y + m23 * v.z
z = m31 * v.x + m32 * v.y + m33 * v.z
w = m41 * v.x + m42 * v.y + m43 * v.z
}
}
operator fun times(v: Vector): Vector = Vector(
x = m11 * v.x + m12 * v.y + m13 * v.z + m14 * v.w,
y = m21 * v.x + m22 * v.y + m23 * v.z + m24 * v.w,
z = m31 * v.x + m32 * v.y + m33 * v.z + m34 * v.w,
w = m41 * v.x + m42 * v.y + m43 * v.z + m44 * v.w,
)
operator fun times(m: Matrix): Matrix = Matrix(
m11 = m11 * m.m11 + m12 * m.m21 + m13 * m.m31 + m14 * m.m41,
m12 = m11 * m.m12 + m12 * m.m22 + m13 * m.m32 + m14 * m.m42,
m13 = m11 * m.m13 + m12 * m.m23 + m13 * m.m33 + m14 * m.m43,
m14 = m11 * m.m14 + m12 * m.m24 + m13 * m.m34 + m14 * m.m44,
m21 = m21 * m.m11 + m22 * m.m21 + m23 * m.m31 + m24 * m.m41,
m22 = m21 * m.m12 + m22 * m.m22 + m23 * m.m32 + m24 * m.m42,
m23 = m21 * m.m13 + m22 * m.m23 + m23 * m.m33 + m24 * m.m43,
m24 = m21 * m.m14 + m22 * m.m24 + m23 * m.m34 + m24 * m.m44,
m31 = m31 * m.m11 + m32 * m.m21 + m33 * m.m31 + m34 * m.m41,
m32 = m31 * m.m12 + m32 * m.m22 + m33 * m.m32 + m34 * m.m42,
m33 = m31 * m.m13 + m32 * m.m23 + m33 * m.m33 + m34 * m.m43,
m34 = m31 * m.m14 + m32 * m.m24 + m33 * m.m34 + m34 * m.m44,
m41 = m41 * m.m11 + m42 * m.m21 + m43 * m.m31 + m44 * m.m41,
m42 = m41 * m.m12 + m42 * m.m22 + m43 * m.m32 + m44 * m.m42,
m43 = m41 * m.m13 + m42 * m.m23 + m43 * m.m33 + m44 * m.m43,
m44 = m41 * m.m14 + m42 * m.m24 + m43 * m.m34 + m44 * m.m44
)
fun transpose(): Matrix = Matrix(
m11 = m11, m12 = m21, m13 = m31, m14 = m41,
m21 = m12, m22 = m22, m23 = m32, m24 = m42,
m31 = m13, m32 = m23, m33 = m33, m34 = m43,
m41 = m14, m42 = m24, m43 = m34, m44 = m44
)
fun inverse(): Matrix {
val transposed = transpose()
val determinant = determinant()
return generate { i, j ->
val minor = transposed.exclude(i, j).determinant()
val adj = if ((i + j) % 2 == 0) minor else -minor
adj / determinant
}
}
companion object {
fun projection(
left: Float,
right: Float,
bottom: Float,
top: Float,
near: Float,
far: Float
): Matrix = Matrix(
m11 = 2 * near / (right - left),
m12 = 0f,
m13 = (right + left) / (right - left),
m14 = 0f,
m21 = 0f,
m22 = 2 * near / (top - bottom),
m23 = (top + bottom) / (top - bottom),
m24 = 0f,
m31 = 0f,
m32 = 0f,
m33 = -(far + near) / (far - near),
m34 = -2 * far * near / (far - near),
m41 = 0f,
m42 = 0f,
m43 = -1f,
m44 = 0f
)
fun rotation(x: Float, y: Float, z: Float, angle: Float): Matrix {
val length = length(x, y, z)
val nx = x / length
val ny = y / length
val nz = z / length
val c = cos(angle)
val s = sin(angle)
return Matrix(
m11 = (1 - c) * nx * nx + c,
m12 = (1 - c) * nx * ny - s * nz,
m13 = (1 - c) * nx * nz + s * ny,
m14 = 0f,
m21 = (1 - c) * nx * ny + s * nz,
m22 = (1 - c) * ny * ny + c,
m23 = (1 - c) * ny * nz - s * nx,
m24 = 0f,
m31 = (1 - c) * nx * nz - s * ny,
m32 = (1 - c) * ny * nz + s * nx,
m33 = (1 - c) * nz * nz + c,
m34 = 0f,
m41 = 0f,
m42 = 0f,
m43 = 0f,
m44 = 1f
)
}
fun translation(x: Float, y: Float, z: Float): Matrix = Matrix(
m11 = 1f, m12 = 0f, m13 = 0f, m14 = x,
m21 = 0f, m22 = 1f, m23 = 0f, m24 = y,
m31 = 0f, m32 = 0f, m33 = 1f, m34 = z,
m41 = 0f, m42 = 0f, m43 = 0f, m44 = 1f
)
fun scale(x: Float, y: Float, z: Float): Matrix = Matrix(
m11 = x, m12 = 0f, m13 = 0f, m14 = 0f,
m21 = 0f, m22 = y, m23 = 0f, m24 = 0f,
m31 = 0f, m32 = 0f, m33 = z, m34 = 0f,
m41 = 0f, m42 = 0f, m43 = 0f, m44 = 1f
)
fun lookAt(dirX: Float, dirY: Float, dirZ: Float): Matrix {
val forward = Vector(dirX, dirY, dirZ, 0f).norm()
val left = (Vector(0f, 1f, 0f, 0f) cross forward).norm()
val up = forward cross left
return Matrix(
m11 = left.x, m12 = up.x, m13 = forward.x, m14 = 0f,
m21 = left.y, m22 = up.y, m23 = forward.y, m24 = 0f,
m31 = left.z, m32 = up.z, m33 = forward.z, m34 = 0f,
m41 = 0f, m42 = 0f, m43 = 0f, m44 = 1f
)
}
fun lookAt(x: Float, y: Float, z: Float, dirX: Float, dirY: Float, dirZ: Float): Matrix {
return translation(x, y, z) * lookAt(dirX, dirY, dirZ)
}
fun generate(generator: (Int, Int) -> Float): Matrix = Matrix(
m11 = generator(0, 0), m12 = generator(0, 1), m13 = generator(0, 2), m14 = generator(0, 3),
m21 = generator(1, 0), m22 = generator(1, 1), m23 = generator(1, 2), m24 = generator(1, 3),
m31 = generator(2, 0), m32 = generator(2, 1), m33 = generator(2, 2), m34 = generator(2, 3),
m41 = generator(3, 0), m42 = generator(3, 1), m43 = generator(3, 2), m44 = generator(3, 3)
)
val identity: Matrix = Matrix(
m11 = 1f, m12 = 0f, m13 = 0f, m14 = 0f,
m21 = 0f, m22 = 1f, m23 = 0f, m24 = 0f,
m31 = 0f, m32 = 0f, m33 = 1f, m34 = 0f,
m41 = 0f, m42 = 0f, m43 = 0f, m44 = 1f
)
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.geometry
import kotlin.jvm.JvmField
class MutableVector {
@JvmField
var x: Float = 0f
@JvmField
var y: Float = 0f
@JvmField
var z: Float = 0f
@JvmField
var w: Float = 0f
fun set(vector: Vector) {
x = vector.x
y = vector.y
z = vector.z
w = vector.w
}
fun normalizeW() {
x /= w
y /= w
z /= w
w = 1f
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.geometry
import kotlin.jvm.JvmField
import kotlin.math.sqrt
class Vector(
@JvmField val x: Float,
@JvmField val y: Float,
@JvmField val z: Float,
@JvmField val w: Float
) {
operator fun unaryMinus(): Vector {
return Vector(-x, -y, -z, -w)
}
override fun toString(): String = "$x, $y, $z, $w"
infix fun cross(other: Vector): Vector = Vector(
x = y * other.z - z * other.y,
y = z * other.x - x * other.z,
z = x * other.y - y * other.x,
w = 0f
)
fun length(): Float = sqrt(x * x + y * y + z * z + w * w)
fun norm(): Vector {
val l = length()
return Vector(x / l, y / l, z / l, w / l)
}
companion object {
val zero: Vector = Vector(0f, 0f, 0f, 1f)
val axisX: Vector = Vector(1f, 0f, 0f, 1f)
val axisY: Vector = Vector(0f, 1f, 0f, 1f)
val axisZ: Vector = Vector(0f, 0f, 1f, 1f)
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.geometry
import kotlin.math.sqrt
fun length(x: Float, y: Float, z: Float): Float = sqrt(x * x + y * y + z * z)

View File

@ -0,0 +1,37 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.rasterization
import kotlin.jvm.JvmField
class Raster(@JvmField val width: Int, @JvmField val height: Int) {
@JvmField var color: IntArray = IntArray(width * height)
@JvmField val depth: FloatArray = FloatArray(width * height)
fun pointer(x: Int, y: Int): Int = y * width + x
fun clear() {
color.fill(255 shl 24)
depth.fill(0f)
}
fun flip(): IntArray {
val result = color
color = IntArray(width * height)
return result
}
}

View File

@ -0,0 +1,184 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.rasterization
import org.teavm.samples.software3d.geometry.Vector
import org.teavm.samples.software3d.geometry.length
import kotlin.math.ceil
class Rasterizer(val raster: Raster, val offset: Int, val step: Int) {
var ambient: Vector = Vector.zero
var lightPosition: Vector = Vector.zero
var lightColor: Vector = Vector.zero
fun drawTriangle(a: VertexParams, b: VertexParams, c: VertexParams) {
val v1: VertexParams
val v2: VertexParams
val v3: VertexParams
if (a.pos.y < b.pos.y) {
if (a.pos.y < c.pos.y) {
v1 = a
if (b.pos.y < c.pos.y) {
v2 = b
v3 = c
} else {
v2 = c
v3 = b
}
} else {
v1 = c
v2 = a
v3 = b
}
} else {
if (b.pos.y < c.pos.y) {
v1 = b
if (c.pos.y < a.pos.y) {
v2 = c
v3 = a
} else {
v2 = a
v3 = c
}
} else {
v1 = c
v2 = b
v3 = a
}
}
val v3x = if (v2.pos.y == v3.pos.y) {
v3.pos.x
} else {
v1.pos.x + (v2.pos.y - v1.pos.y) * (v3.pos.x - v1.pos.x) / (v3.pos.y - v1.pos.y)
}
if (v2.pos.x < v3x) {
drawTrianglePart(v1, v2, v1, v3, ceil(v1.pos.y).toInt(), ceil(v2.pos.y).toInt())
drawTrianglePart(v2, v3, v1, v3, ceil(v2.pos.y).toInt(), ceil(v3.pos.y).toInt())
} else {
drawTrianglePart(v1, v3, v1, v2, ceil(v1.pos.y).toInt(), ceil(v2.pos.y).toInt())
drawTrianglePart(v1, v3, v2, v3, ceil(v2.pos.y).toInt(), ceil(v3.pos.y).toInt())
}
}
private fun normalizeY(y: Int): Int = ((y + step - 1 - offset) / step) * step + offset
private fun drawTrianglePart(
s1: VertexParams, e1: VertexParams,
s2: VertexParams, e2: VertexParams,
sy: Int, ey: Int
) {
val nsy = normalizeY(sy).coerceAtLeast(0)
val ney = normalizeY(ey).coerceAtMost(raster.height * step)
if (ney <= 0 || nsy >= raster.height * step) {
return
}
val d1x = e1.pos.x - s1.pos.x
val d1y = e1.pos.y - s1.pos.y
val d1z = e1.pos.z - s1.pos.z
val d2x = e2.pos.x - s2.pos.x
val d2y = e2.pos.y - s2.pos.y
val d2z = e2.pos.z - s2.pos.z
var y = nsy
while (y < ney) {
val k1 = if (d1y == 0f) 0f else (y - s1.pos.y) / d1y
val k2 = if (d2y == 0f) 0f else (y - s2.pos.y) / d2y
val sx = s1.pos.x + d1x * k1
val ex = s2.pos.x + d2x * k2
val startIntX = ceil(sx).toInt().coerceAtLeast(0)
val endIntX = ceil(ex).toInt().coerceAtMost(raster.width)
if (startIntX + 1 == endIntX || startIntX >= raster.width || endIntX <= 0) {
y += step
continue
}
val sz = s1.pos.z + d1z * k1
val sar = s1.ambient.x + (e1.ambient.x - s1.ambient.x) * k1
val sag = s1.ambient.y + (e1.ambient.y - s1.ambient.y) * k1
val sab = s1.ambient.z + (e1.ambient.z - s1.ambient.z) * k1
val sdr = s1.diffuse.x + (e1.diffuse.x - s1.diffuse.x) * k1
val sdg = s1.diffuse.y + (e1.diffuse.y - s1.diffuse.y) * k1
val sdb = s1.diffuse.z + (e1.diffuse.z - s1.diffuse.z) * k1
val snx = s1.normal.x + (e1.normal.x - s1.normal.x) * k1
val sny = s1.normal.y + (e1.normal.y - s1.normal.y) * k1
val snz = s1.normal.z + (e1.normal.z - s1.normal.z) * k1
val sox = s1.orig.x + (e1.orig.x - s1.orig.x) * k1
val soy = s1.orig.y + (e1.orig.y - s1.orig.y) * k1
val soz = s1.orig.z + (e1.orig.z - s1.orig.z) * k1
val ez = s2.pos.z + d2z * k2
val ear = s2.ambient.x + (e2.ambient.x - s2.ambient.x) * k2
val eag = s2.ambient.y + (e2.ambient.y - s2.ambient.y) * k2
val eab = s2.ambient.z + (e2.ambient.z - s2.ambient.z) * k2
val edr = s2.diffuse.x + (e2.diffuse.x - s2.diffuse.x) * k2
val edg = s2.diffuse.y + (e2.diffuse.y - s2.diffuse.y) * k2
val edb = s2.diffuse.z + (e2.diffuse.z - s2.diffuse.z) * k2
val enx = s2.normal.x + (e2.normal.x - s2.normal.x) * k2
val eny = s2.normal.y + (e2.normal.y - s2.normal.y) * k2
val enz = s2.normal.z + (e2.normal.z - s2.normal.z) * k2
val eox = s2.orig.x + (e2.orig.x - s2.orig.x) * k2
val eoy = s2.orig.y + (e2.orig.y - s2.orig.y) * k2
val eoz = s2.orig.z + (e2.orig.z - s2.orig.z) * k2
var ptr = raster.pointer(startIntX, y / step)
var x = startIntX
while (x < endIntX) {
val z = sz + (ez - sz) * (x - sx) / (ex - sx)
if (z > raster.depth[ptr]) {
raster.depth[ptr] = z
val k = if (sx == ex) 0f else (x - sx) / (ex - sx)
val ar = sar + (ear - sar) * k
val ag = sag + (eag - sag) * k
val ab = sab + (eab - sab) * k
val dr = sdr + (edr - sdr) * k
val dg = sdg + (edg - sdg) * k
val db = sdb + (edb - sdb) * k
val nx = snx + (enx - snx) * k
val ny = sny + (eny - sny) * k
val nz = snz + (enz - snz) * k
val ox = sox + (eox - sox) * k
val oy = soy + (eoy - soy) * k
val oz = soz + (eoz - soz) * k
val lightDirX = lightPosition.x - ox
val lightDirY = lightPosition.y - oy
val lightDirZ = lightPosition.z - oz
val lightDirLength = length(lightDirX, lightDirY, lightDirZ)
val normalLength = length(nx, ny, nz)
var cosAngle = (nx * lightDirX + ny * lightDirY + nz * lightDirZ) / (lightDirLength * normalLength)
cosAngle = cosAngle.coerceAtLeast(0f)
val r = ar * ambient.x + dr * lightColor.x * cosAngle
val g = ag * ambient.y + dg * lightColor.y * cosAngle
val b = ab * ambient.z + db * lightColor.z * cosAngle
val intR = (r * 255).toInt().coerceIn(0, 255)
val intG = (g * 255).toInt().coerceIn(0, 255)
val intB = (b * 255).toInt().coerceIn(0, 255)
raster.color[ptr] = intB or (intG shl 8) or (intR shl 16) or (255 shl 24)
}
++ptr
++x
}
y += step
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.rasterization
import org.teavm.samples.software3d.geometry.MutableVector
import org.teavm.samples.software3d.geometry.Vector
import kotlin.jvm.JvmField
class VertexParams {
@JvmField
val pos: MutableVector = MutableVector()
@JvmField
val orig: MutableVector = MutableVector()
@JvmField
val normal: MutableVector = MutableVector()
@JvmField
var ambient: Vector = Vector.zero
@JvmField
var diffuse: Vector = Vector.zero
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.rendering
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.rasterization.Raster
import org.teavm.samples.software3d.rasterization.Rasterizer
import org.teavm.samples.software3d.rasterization.VertexParams
import org.teavm.samples.software3d.scene.Item
import org.teavm.samples.software3d.scene.Scene
class Renderer(val scene: Scene, val raster: Raster, offset: Int, step: Int) {
private val rasterizer = Rasterizer(raster, offset, step)
var projection: Matrix = Matrix.identity
var viewport: Matrix = Matrix.identity
private var transform: Matrix = Matrix.identity
private var v1 = VertexParams()
private var v2 = VertexParams()
private var v3 = VertexParams()
fun render() {
transform = viewport * projection * scene.camera
raster.clear()
rasterizer.lightPosition = scene.camera * scene.lightPosition
rasterizer.lightColor = scene.lightColor
rasterizer.ambient = scene.ambientColor
for (item in scene.items) {
renderItem(item)
}
}
private fun renderItem(item: Item) {
val itemTransform = scene.camera * item.transform
val viewItemTransform = transform * item.transform
for (face in item.mesh.faces) {
itemTransform.applyTo(face.a.position, v1.orig)
itemTransform.applyTo(face.b.position, v2.orig)
itemTransform.applyTo(face.c.position, v3.orig)
viewItemTransform.applyTo(face.a.position, v1.pos)
viewItemTransform.applyTo(face.b.position, v2.pos)
viewItemTransform.applyTo(face.c.position, v3.pos)
itemTransform.applyDirTo(face.a.normal, v1.normal)
itemTransform.applyDirTo(face.b.normal, v2.normal)
itemTransform.applyDirTo(face.c.normal, v3.normal)
v1.diffuse = face.a.diffuse
v1.ambient = face.a.ambient
v2.diffuse = face.b.diffuse
v2.ambient = face.b.ambient
v3.diffuse = face.c.diffuse
v3.ambient = face.c.ambient
v1.pos.normalizeW()
v2.pos.normalizeW()
v3.pos.normalizeW()
rasterizer.drawTriangle(v1, v2, v3)
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.scene
import org.teavm.samples.software3d.geometry.Matrix
class Item(val mesh: Mesh) {
var transform: Matrix = Matrix.identity
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.scene
import org.teavm.samples.software3d.geometry.Face
class Mesh(
val faces: List<Face>
)

View File

@ -0,0 +1,28 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.scene
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.geometry.Vector
class Scene {
val items: MutableList<Item> = mutableListOf()
var camera: Matrix = Matrix.identity
var lightPosition: Vector = Vector.zero
var lightColor: Vector = Vector.zero
var ambientColor: Vector = Vector.zero
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.scene
import org.teavm.samples.software3d.geometry.Vector
class Vertex(
val position: Vector,
val normal: Vector,
val ambient: Vector,
val diffuse: Vector
)

View File

@ -0,0 +1,55 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.scenes
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.geometry.Vector
import org.teavm.samples.software3d.scene.Item
import org.teavm.samples.software3d.scene.Scene
import kotlin.math.PI
fun geometry(): Pair<Scene, (Double) -> Unit> {
val scene = Scene()
val cubeConstantTransform = Matrix.scale(1.4f, 1.4f, 1.4f) * Matrix.rotation(1f, 0f, 0f, PI.toFloat() / 4)
val cubeItem = Item(Meshes.cube())
scene.items += cubeItem
val sphere1 = Item(Meshes.sphere())
val sphere1ConstantTransform = Matrix.translation(2f, 0f, 0f) * Matrix.scale(0.2f, 0.2f, 0.2f)
scene.items += sphere1
val sphere2 = Item(Meshes.sphere())
val sphere2ConstantTransform = Matrix.translation(-3.5f, 0f, 0f) * Matrix.scale(0.3f, 0.3f, 0.3f)
scene.items += sphere2
scene.camera = Matrix.lookAt(0f, 2f, -7f, 0f, -0.3f, 1f).inverse()
scene.lightColor = Vector(0.5f, 0.5f, 0.5f, 0.5f)
scene.lightPosition = Vector(3f, 0.5f, -2f, 1f)
scene.ambientColor = Vector(0.5f, 0.5f, 0.5f, 0.5f)
return Pair(scene) { time ->
val cubeVarTransform = Matrix.rotation(0f, 1f, 0f, (PI * time / 3).toFloat())
cubeItem.transform = cubeVarTransform * cubeConstantTransform
val sphere1VarTransform = Matrix.rotation(0f, 1f, -0.5f, (-PI * time).toFloat())
sphere1.transform = sphere1VarTransform * sphere1ConstantTransform
val sphere2VarTransform = Matrix.rotation(0.5f, 1f, 0.5f, (-PI * time / 10).toFloat())
sphere2.transform = sphere2VarTransform * sphere2ConstantTransform
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.scenes
import org.teavm.samples.software3d.geometry.Face
import org.teavm.samples.software3d.geometry.Vector
import org.teavm.samples.software3d.scene.Mesh
import org.teavm.samples.software3d.scene.Vertex
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
object Meshes {
fun cube(): Mesh {
val positions = listOf(
Vector(-1f, -1f, -1f, 1f),
Vector(1f, -1f, -1f, 1f),
Vector(1f, 1f, -1f, 1f),
Vector(-1f, 1f, -1f, 1f),
Vector(-1f, -1f, 1f, 1f),
Vector(1f, -1f, 1f, 1f),
Vector(1f, 1f, 1f, 1f),
Vector(-1f, 1f, 1f, 1f)
)
val red = Vector(1f, 0f, 0f, 1f)
val green = Vector(0f, 1f, 0f, 1f)
val blue = Vector(0f, 0f, 1f, 1f)
val yellow = Vector(1f, 1f, 0f, 1f)
val cyan = Vector(0f, 1f, 1f, 1f)
val magenta = Vector(1f, 0f, 1f, 1f)
return Mesh(listOf(
Face(
Vertex(positions[0], -Vector.axisZ, red, red),
Vertex(positions[1], -Vector.axisZ, red, red),
Vertex(positions[2], -Vector.axisZ, red, red)
),
Face(
Vertex(positions[0], -Vector.axisZ, red, red),
Vertex(positions[2], -Vector.axisZ, red, red),
Vertex(positions[3], -Vector.axisZ, red, red)
),
Face(
Vertex(positions[4], Vector.axisZ, cyan, cyan),
Vertex(positions[5], Vector.axisZ, cyan, cyan),
Vertex(positions[6], Vector.axisZ, cyan, cyan)
),
Face(
Vertex(positions[4], Vector.axisZ, cyan, cyan),
Vertex(positions[7], Vector.axisZ, cyan, cyan),
Vertex(positions[6], Vector.axisZ, cyan, cyan)
),
Face(
Vertex(positions[0], -Vector.axisY, yellow, yellow),
Vertex(positions[1], -Vector.axisY, yellow, yellow),
Vertex(positions[5], -Vector.axisY, yellow, yellow)
),
Face(
Vertex(positions[0], -Vector.axisY, yellow, yellow),
Vertex(positions[4], -Vector.axisY, yellow, yellow),
Vertex(positions[5], -Vector.axisY, yellow, yellow)
),
Face(
Vertex(positions[2], Vector.axisY, blue, blue),
Vertex(positions[3], Vector.axisY, blue, blue),
Vertex(positions[7], Vector.axisY, blue, blue)
),
Face(
Vertex(positions[2], Vector.axisY, blue, blue),
Vertex(positions[6], Vector.axisY, blue, blue),
Vertex(positions[7], Vector.axisY, blue, blue)
),
Face(
Vertex(positions[0], -Vector.axisX, green, green),
Vertex(positions[3], -Vector.axisX, green, green),
Vertex(positions[4], -Vector.axisX, green, green)
),
Face(
Vertex(positions[3], -Vector.axisX, green, green),
Vertex(positions[4], -Vector.axisX, green, green),
Vertex(positions[7], -Vector.axisX, green, green)
),
Face(
Vertex(positions[1], Vector.axisX, magenta, magenta),
Vertex(positions[2], Vector.axisX, magenta, magenta),
Vertex(positions[5], Vector.axisX, magenta, magenta)
),
Face(
Vertex(positions[2], Vector.axisX, magenta, magenta),
Vertex(positions[5], Vector.axisX, magenta, magenta),
Vertex(positions[6], Vector.axisX, magenta, magenta)
)
))
}
fun sphere(): Mesh {
val count = 5
val rows = count * 4
val columns = count * 4
val color = Vector(1f, 1f, 1f, 1f)
val vertices = (0 until rows).map { i ->
val latitude = i * PI / 2 / count
val latX = cos(latitude)
val latY = sin(latitude)
(0 until columns).map { j ->
val longitude = j * PI / 2 / count
val longX = cos(longitude)
val longY = sin(longitude)
val pos = Vector(
x = (longX * latX).toFloat(),
z = (longY * latX).toFloat(),
y = latY.toFloat(),
w = 1f
)
Vertex(
position = pos,
normal = pos,
ambient = color,
diffuse = color
)
}
}
val faces = (0 until rows).flatMap { row ->
(0 until columns).flatMap { col ->
val a = vertices[row][col]
val b = vertices[(row + 1) % rows][col]
val c = vertices[(row + 1) % rows][(col + 1) % columns]
val d = vertices[row][(col + 1) % columns]
listOf(
Face(a, b, c),
Face(a, c, d)
)
}
}
return Mesh(faces)
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.kjs
import org.khronos.webgl.Int32Array
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.rasterization.Raster
import org.teavm.samples.software3d.rendering.Renderer
import org.teavm.samples.software3d.scenes.geometry
import org.w3c.dom.DedicatedWorkerGlobalScope
fun main() {
self.onmessage = { event ->
val data = event.data.asDynamic()
when (data.type as String) {
"init" -> init(data)
"frame" -> renderFrame(data)
"stop" -> self.close()
}
Unit
}
}
private fun init(data: dynamic) {
val width = data.width as Int
val height = data.height as Int
val step = data.step as Int
val offset = data.offset as Int
val (scene, updaterF) = geometry()
raster = Raster(width, height / step)
updater = updaterF
renderer = Renderer(scene, raster, offset, step).apply {
projection = Matrix.projection(-1f, 1f, -1f, 1f, 2f, 10f)
viewport = Matrix.translation(width / 2f, height / 2f, 0f) *
Matrix.scale(width / 2f, width / 2f, 1f)
}
}
private fun renderFrame(data: dynamic) {
val time = data.time as Double
val perfStart = self.performance.now()
updater(time)
renderer.render()
val perfEnd = self.performance.now()
val typedArray = raster.flip().asDynamic() as Int32Array
val message = Any().asDynamic()
message.data = typedArray.buffer
message.time = ((perfEnd - perfStart) * 1000000).toInt()
self.postMessage(message, arrayOf(typedArray.buffer))
}
private val self = js("self") as DedicatedWorkerGlobalScope
private lateinit var raster: Raster
private lateinit var renderer: Renderer
private lateinit var updater: (Double) -> Unit

View File

@ -0,0 +1,115 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.swing
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.rasterization.Raster
import org.teavm.samples.software3d.rendering.Renderer
import org.teavm.samples.software3d.scenes.geometry
import java.awt.*
import java.awt.image.BufferedImage
import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue
import javax.swing.JComponent
import javax.swing.JFrame
import javax.swing.WindowConstants
private const val SCENE_WIDTH = 1024
private const val SCENE_HEIGHT = 768
fun main() {
val image = BufferedImage(SCENE_WIDTH, SCENE_HEIGHT, BufferedImage.TYPE_INT_ARGB)
val imageComponent = ImageComponent(image)
val window = JFrame().apply {
defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
add(imageComponent)
pack()
isVisible = true
}
EventQueue.invokeAndWait {
window.pack()
}
val (scene, updater) = geometry()
val taskCount = Runtime.getRuntime().availableProcessors()
println("Running on $taskCount CPUs")
val rasters = (0 until taskCount).map { Raster(SCENE_WIDTH, SCENE_HEIGHT / taskCount) }
val renderers = rasters.mapIndexed() { index, raster ->
Renderer(scene, raster, index, taskCount).apply {
projection = Matrix.projection(-1f, 1f, -1f, 1f, 2f, 10f)
viewport = Matrix.translation(SCENE_WIDTH / 2f, SCENE_HEIGHT / 2f, 0f) *
Matrix.scale(SCENE_WIDTH / 2f, SCENE_WIDTH / 2f, 1f)
}
}
val queues = renderers.map { renderer ->
val queue = LinkedBlockingQueue<CountDownLatch>()
Thread {
while (true) {
val latch = queue.take()
renderer.render()
latch.countDown()
}
}.apply {
isDaemon = true
start()
}
queue
}
val startTime = System.currentTimeMillis()
var totalTime = 0L
var totalTimeSec = 0L
var frames = 1
while (true) {
val frameStart = System.currentTimeMillis()
val frameStartPerf = System.nanoTime()
val currentTime = frameStart - startTime
updater(currentTime / 1000.0)
val latch = CountDownLatch(taskCount)
queues.forEach { it.offer(latch) }
latch.await()
val frameEndPerf = System.nanoTime()
totalTime += (frameEndPerf - frameStartPerf) / 1000
++frames
if (totalTime / 1000000 != totalTimeSec) {
totalTimeSec = totalTime / 1000000
println("Average render time ${totalTime / frames}")
}
EventQueue.invokeAndWait {
for (y in 0 until SCENE_HEIGHT) {
val raster = rasters[y % taskCount]
val start = raster.pointer(0, y / taskCount)
image.setRGB(0, y, SCENE_WIDTH, 1, raster.color, start, SCENE_WIDTH)
}
imageComponent.repaint()
}
}
}
private class ImageComponent(private val image: BufferedImage) : JComponent() {
override fun paint(g: Graphics) {
g as Graphics2D
g.drawImage(image as Image, 0, 0, null)
}
override fun getPreferredSize(): Dimension = Dimension(SCENE_WIDTH, SCENE_HEIGHT)
override fun getMinimumSize(): Dimension = getPreferredSize()
}

View File

@ -0,0 +1,124 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.teavm
import org.teavm.jso.JSObject
import org.teavm.jso.browser.Window
import org.teavm.jso.canvas.CanvasRenderingContext2D
import org.teavm.jso.canvas.ImageData
import org.teavm.jso.core.JSMapLike
import org.teavm.jso.core.JSNumber
import org.teavm.jso.core.JSObjects
import org.teavm.jso.core.JSString
import org.teavm.jso.dom.html.HTMLCanvasElement
import org.teavm.jso.dom.html.HTMLDocument
import org.teavm.jso.typedarrays.ArrayBuffer
import org.teavm.jso.typedarrays.Uint8ClampedArray
import org.teavm.jso.workers.Worker
import org.teavm.samples.software3d.util.PerformanceMeasure
class Controller(
private val width: Int,
private val height: Int,
private val onPerformance: (Int, Long) -> Unit,
) {
private val canvas = HTMLDocument.current().getElementById("canvas") as HTMLCanvasElement
private val context = canvas.getContext("2d") as CanvasRenderingContext2D
private val startTime = System.currentTimeMillis()
private var onRenderComplete: (Int, ArrayBuffer) -> Unit = { _, _ -> }
private var workers: List<Worker> = emptyList()
private var performanceMeasureByWorker: List<PerformanceMeasure> = emptyList()
private val performanceMeasure = PerformanceMeasure(100000) { onPerformance(-1, it) }
val tasks: Int get() = workers.size
fun startRendering(tasks: Int, workerType: WorkerType) {
performanceMeasure.reset()
stopRendering()
val scriptName = when (workerType) {
WorkerType.JS -> "js-worker.js"
WorkerType.WEBASSEMBLY -> "wasm-worker.js"
WorkerType.KOTLIN_JS -> "kjs/software3d.js"
}
workers = (0 until tasks).map { index ->
Worker.create(scriptName).apply {
postMessage(JSObjects.createWithoutProto<JSMapLike<JSObject>>().apply {
set("type", JSString.valueOf("init"))
set("width", JSNumber.valueOf(width))
set("height", JSNumber.valueOf(height))
set("step", JSNumber.valueOf(tasks))
set("offset", JSNumber.valueOf(index))
})
onMessage { event ->
if (index < workers.size && workers[index] == event.target) {
val data = event.data as JSMapLike<*>
val buffer = data["data"] as ArrayBuffer
onRenderComplete(index, buffer)
performanceMeasureByWorker[index].reportFrame((data["time"] as JSNumber).intValue().toLong())
}
}
}
}
performanceMeasureByWorker = (0 until tasks).map { index ->
PerformanceMeasure(100000) { onPerformance(index, it) }
}
renderFrame()
}
private fun stopRendering() {
workers.forEach {
it.postMessage(JSObjects.createWithoutProto<JSMapLike<JSObject>>().apply {
set("type", JSString.valueOf("stop"))
})
}
workers = emptyList()
}
private fun renderFrame() {
val currenTime = System.currentTimeMillis()
val frameTime = (currenTime - startTime) / 1000.0
var pending = workers.size
val buffers = arrayOfNulls<ArrayBuffer>(workers.size)
performanceMeasure.startFrame()
for (worker in workers) {
worker.postMessage(JSObjects.createWithoutProto<JSMapLike<JSObject>>().apply {
set("type", JSString.valueOf("frame"))
set("time", JSNumber.valueOf(frameTime))
})
}
onRenderComplete = { index, buffer ->
buffers[index] = buffer
if (--pending == 0) {
performanceMeasure.endFrame()
Window.requestAnimationFrame {
displayBuffers(buffers)
renderFrame()
}
}
}
}
private fun displayBuffers(buffers: Array<out ArrayBuffer?>) {
for (y in 0 until height) {
val buffer = buffers[y % buffers.size]!!
val array = Uint8ClampedArray.create(buffer, width * 4 * (y / buffers.size), width * 4)
val imageData = ImageData.create(array, width, 1)
context.putImageData(imageData, 0.0, y.toDouble())
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.teavm
import org.teavm.interop.Address
import org.teavm.interop.Export
import org.teavm.interop.Import
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.rasterization.Raster
import org.teavm.samples.software3d.rendering.Renderer
import org.teavm.samples.software3d.scenes.geometry
fun main() {
println("Worker started")
}
@Export(name = "initWorker")
fun init(width: Int, height: Int, step: Int, offset: Int) {
val (scene, updaterF) = geometry()
raster = Raster(width, height / step)
updater = updaterF
renderer = Renderer(scene, raster, offset, step).apply {
projection = Matrix.projection(-1f, 1f, -1f, 1f, 2f, 10f)
viewport = Matrix.translation(width / 2f, height / 2f, 0f) *
Matrix.scale(width / 2f, width / 2f, 1f)
}
println("Worker initialized")
}
@Export(name = "renderFrame")
fun renderFrame(time: Double) {
val perfStart = System.nanoTime()
updater(time)
renderer.render()
val perfEnd = System.nanoTime()
val buffer = raster.color
sendRenderResult(Address.ofData(buffer), buffer.size * 4, (perfEnd - perfStart).toInt())
}
@Import(module = "renderer", name = "result")
external fun sendRenderResult(data: Address, dataSize: Int, time: Int)
private lateinit var raster: Raster
private lateinit var renderer: Renderer
private lateinit var updater: (Double) -> Unit

View File

@ -0,0 +1,73 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.teavm
import org.teavm.jso.JSObject
import org.teavm.jso.browser.Window
import org.teavm.jso.core.*
import org.teavm.samples.software3d.geometry.Matrix
import org.teavm.samples.software3d.rasterization.Raster
import org.teavm.samples.software3d.rendering.Renderer
import org.teavm.samples.software3d.scenes.geometry
fun worker() {
val worker = RenderWorker()
Window.worker().listenMessage {
val dataJson = it.data as JSMapLike<*>
when ((dataJson["type"] as JSString).stringValue()) {
"init" -> worker.init(dataJson)
"frame" -> worker.renderFrame(dataJson)
"stop" -> Window.worker().close()
}
}
}
class RenderWorker {
private lateinit var raster: Raster
private lateinit var renderer: Renderer
private lateinit var updater: (Double) -> Unit
private var width: Int = 0
private var height: Int = 0
fun init(params: JSMapLike<*>) {
val (scene, updaterF) = geometry()
width = (params["width"] as JSNumber).intValue()
height = (params["height"] as JSNumber).intValue()
val step = (params["step"] as JSNumber).intValue()
val offset = (params["offset"] as JSNumber).intValue()
raster = Raster(width, height / step)
updater = updaterF
renderer = Renderer(scene, raster, offset, step).apply {
projection = Matrix.projection(-1f, 1f, -1f, 1f, 2f, 10f)
viewport = Matrix.translation(width / 2f, height / 2f, 0f) *
Matrix.scale(width / 2f, width / 2f, 1f)
}
}
fun renderFrame(params: JSMapLike<*>) {
val time = (params["time"] as JSNumber).doubleValue()
val perfStart = System.nanoTime()
updater(time)
renderer.render()
val perfEnd = System.nanoTime()
val buffer = extractBuffer(raster.flip())
postMessageFromWorker(JSObjects.createWithoutProto<JSMapLike<JSObject>>().apply {
set("data", buffer)
set("time", JSNumber.valueOf((perfEnd - perfStart).toInt()))
}, JSArray.of(buffer))
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.teavm
enum class WorkerType {
JS,
WEBASSEMBLY,
KOTLIN_JS
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.teavm
import org.teavm.jso.dom.html.HTMLDocument
import org.teavm.jso.dom.html.HTMLElement
import org.teavm.jso.dom.html.HTMLOptionElement
import org.teavm.jso.dom.html.HTMLSelectElement
private const val SCENE_WIDTH = 1024
private const val SCENE_HEIGHT = 768
fun main(args: Array<out String>) {
if (args.size == 1 && args[0] == "worker") {
worker()
} else {
runController()
}
}
fun runController() {
val performanceIndicator = HTMLDocument.current().getElementById("performance-indicator")
var performanceIndicatorByWorkers: List<HTMLElement> = emptyList()
val maxWorkers = cpuCount()
var workerType = WorkerType.JS
val controller = Controller(SCENE_WIDTH, SCENE_HEIGHT) { index, value ->
if (index == -1) {
performanceIndicator.innerText = value.toString()
} else {
performanceIndicatorByWorkers.getOrNull(index)?.let {
it.innerText = value.toString()
}
}
}
performanceIndicatorByWorkers = recreatePerformanceIndicators(maxWorkers)
controller.startRendering(maxWorkers, workerType)
val cpuSelector = HTMLDocument.current().getElementById("workers") as HTMLSelectElement
for (i in 1..maxWorkers) {
val option = HTMLDocument.current().createElement("option") as HTMLOptionElement
option.value = i.toString()
option.text = i.toString()
cpuSelector.appendChild(option)
}
cpuSelector.value = maxWorkers.toString()
cpuSelector.addEventListener("change") {
val newValue = cpuSelector.value.toInt()
if (controller.tasks != newValue) {
controller.startRendering(newValue, workerType)
performanceIndicatorByWorkers = recreatePerformanceIndicators(newValue)
}
}
val workerSelector = HTMLDocument.current().getElementById("worker-type") as HTMLSelectElement
workerSelector.addEventListener("change") {
val newValue = when (workerSelector.value) {
"webassembly" -> WorkerType.WEBASSEMBLY
"kjs" -> WorkerType.KOTLIN_JS
else -> WorkerType.JS
}
if (workerType != newValue) {
workerType = newValue
controller.startRendering(controller.tasks, workerType)
}
}
}
private fun recreatePerformanceIndicators(count: Int): List<HTMLElement> {
val container = HTMLDocument.current().getElementById("performance-indicators-by-workers")
while (container.hasChildNodes()) {
container.removeChild(container.firstChild)
}
return (0 until count).map {
HTMLDocument.current().createElement("li").also {
container.appendChild(it)
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.teavm
import org.teavm.jso.JSBody
import org.teavm.jso.JSByRef
import org.teavm.jso.JSObject
import org.teavm.jso.core.JSArray
import org.teavm.jso.typedarrays.ArrayBuffer
@JSBody(params = ["data"], script = "return data.buffer;")
external fun extractBuffer(@JSByRef data: IntArray): ArrayBuffer
@JSBody(params = ["message", "transferable"], script = "self.postMessage(message, transferable);")
external fun postMessageFromWorker(message: JSObject, transferable: JSArray<out JSObject>)
@JSBody(script = "return navigator.hardwareConcurrency;")
external fun cpuCount(): Int

View File

@ -0,0 +1,48 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.samples.software3d.util
class PerformanceMeasure(private val measureInterval: Long, private val update: (Long) -> Unit) {
private var totalPerfTime = 0L
private var totalPertTimeSecond = 0L
private var frameCount = 0
private var startPerfTime = 0L
fun reset() {
totalPerfTime = 0L
totalPertTimeSecond = 0L
frameCount = 0
}
fun startFrame() {
startPerfTime = System.nanoTime()
}
fun endFrame() {
val endPerfTime = System.nanoTime()
reportFrame(endPerfTime - startPerfTime)
}
fun reportFrame(frameTime: Long) {
totalPerfTime += frameTime / 1000
frameCount++
if (totalPerfTime / measureInterval != totalPertTimeSecond) {
totalPertTimeSecond = totalPerfTime / measureInterval
update(totalPerfTime / frameCount)
}
}
}

View File

@ -0,0 +1,37 @@
<!--
Copyright 2014 Alexey Andreev.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<title>Software 3D renderer</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<script type="text/javascript" charset="utf-8" src="js/software3d.js"></script>
</head>
<body onload="main()">
<div>
<canvas id="canvas" width="1024" height="768"></canvas>
</div>
<div><label for="worker-type">Worker type:</label> <select id="worker-type">
<option value="js">TeaVM JS</option>
<option value="webassembly">TeaVM WebAssembly</option>
<option value="kjs">Kotlin/JS</option>
</select></div>
<div><label for="workers">Workers:</label> <select id="workers"></select></div>
<div>Average frame rendering time, microseconds: <span id="performance-indicator"></span></div>
<div>By worker:</div>
<ol id="performance-indicators-by-workers"></ol>
</body>
</html>

View File

@ -0,0 +1,18 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
importScripts("js/software3d.js");
main(["worker"])

View File

@ -0,0 +1,67 @@
/*
* Copyright 2023 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
importScripts("wasm/software3d.wasm-runtime.js");
let instance = null;
let pendingInstanceFunctions = [];
addEventListener("message", e => {
let data = e.data
switch (data.type) {
case "init":
pendingInstanceFunctions.push(() => {
instance.exports.initWorker(data.width, data.height, data.step, data.offset)
});
runPendingFunctions();
break;
case "frame":
pendingInstanceFunctions.push(() => {
instance.exports.renderFrame(data.time);
});
runPendingFunctions();
break;
case "stop":
self.close();
break;
}
});
TeaVM.wasm.load("wasm/software3d.wasm", {
installImports(o, controller) {
o.renderer = {
result(data, size, time) {
let buffer = controller.instance.exports.memory.buffer.slice(data, data + size);
self.postMessage({ data: buffer, time: time });
}
}
},
}).then(teavm => {
teavm.main([]);
instance = teavm.instance;
runPendingFunctions();
});
function runPendingFunctions() {
if (instance === null) {
return;
}
for (let f of pendingInstanceFunctions) {
f();
}
pendingInstanceFunctions = [];
}