aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Barnett <noreply@jamesbarnett.xyz>2020-12-30 23:13:40 +0000
committerJames Barnett <noreply@jamesbarnett.xyz>2020-12-30 23:13:40 +0000
commit3c3322ef1a7aca3517ff94f723004fb809dec6cd (patch)
treefd195eeedbb03cea8b25795b5e3c8fcf1a10c05c
parentc05b68f786715b20d0a9aef6538141c4227642ae (diff)
downloadkotlin-raycaster-3c3322ef1a7aca3517ff94f723004fb809dec6cd.tar.xz
kotlin-raycaster-3c3322ef1a7aca3517ff94f723004fb809dec6cd.zip
Add adjustable render options. Update UI
-rw-r--r--src/main/kotlin/Camera.kt4
-rw-r--r--src/main/kotlin/CameraController.kt15
-rw-r--r--src/main/kotlin/Minimap.kt11
-rw-r--r--src/main/kotlin/RaycastContext.kt1
-rw-r--r--src/main/kotlin/Raycaster.kt17
-rw-r--r--src/main/kotlin/Renderer.kt10
-rw-r--r--src/main/kotlin/Ui.kt58
-rw-r--r--src/main/kotlin/main.kt8
-rw-r--r--src/main/resources/index.html81
-rw-r--r--src/main/resources/style.css45
10 files changed, 203 insertions, 47 deletions
diff --git a/src/main/kotlin/Camera.kt b/src/main/kotlin/Camera.kt
index 4eb8d43..3c2e582 100644
--- a/src/main/kotlin/Camera.kt
+++ b/src/main/kotlin/Camera.kt
@@ -1,6 +1,6 @@
data class Camera(
- val fov: Int,
+ var fov: Int,
var xPos: Double,
var yPos: Double,
- var rotation: Double
+ var rotation: Double,
) \ No newline at end of file
diff --git a/src/main/kotlin/CameraController.kt b/src/main/kotlin/CameraController.kt
index 436b34b..510f1a0 100644
--- a/src/main/kotlin/CameraController.kt
+++ b/src/main/kotlin/CameraController.kt
@@ -1,4 +1,5 @@
import kotlinx.browser.document
+import org.w3c.dom.HTMLButtonElement
class CameraController(
private val camera: Camera,
@@ -8,18 +9,20 @@ class CameraController(
) {
init {
- document.onkeydown = { inputHandler(it.code) }
+ document.onkeydown = { keyHandler(it.code) }
+ (document.getElementById("up") as HTMLButtonElement).onclick = { moveForward() }
+ (document.getElementById("down") as HTMLButtonElement).onclick = { moveBack() }
+ (document.getElementById("left") as HTMLButtonElement).onclick = { rotateAntiClockwise() }
+ (document.getElementById("right") as HTMLButtonElement).onclick = { rotateClockwise() }
}
- private fun inputHandler(code: String) {
+ private fun keyHandler(code: String) {
when (code) {
"KeyW" -> moveForward()
"KeyS" -> moveBack()
"KeyA" -> rotateAntiClockwise()
"KeyD" -> rotateClockwise()
}
- afterInput()
- console.log("x: ${camera.xPos} y: ${camera.yPos} r: ${camera.rotation}")
}
private fun moveForward() {
@@ -27,6 +30,7 @@ class CameraController(
val cameraSin = camera.rotation.sine() * moveSpeed
camera.xPos += cameraCos
camera.yPos += cameraSin
+ afterInput()
}
private fun moveBack() {
@@ -34,14 +38,17 @@ class CameraController(
val cameraSin = camera.rotation.sine() * moveSpeed
camera.xPos -= cameraCos
camera.yPos -= cameraSin
+ afterInput()
}
private fun rotateClockwise() {
camera.rotation += rotateSpeed
+ afterInput()
}
private fun rotateAntiClockwise() {
camera.rotation -= rotateSpeed
+ afterInput()
}
} \ No newline at end of file
diff --git a/src/main/kotlin/Minimap.kt b/src/main/kotlin/Minimap.kt
index 34862e0..7cbc892 100644
--- a/src/main/kotlin/Minimap.kt
+++ b/src/main/kotlin/Minimap.kt
@@ -3,26 +3,21 @@ import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
class Minimap(private val map: Map) {
- private val scale = 20
+ private val scale = 15
- private val canvas = (document.createElement("canvas") as HTMLCanvasElement)
+ private val canvas = (document.getElementById("minimap") as HTMLCanvasElement)
.apply {
width = map.width * scale
height = map.height * scale
- id = "minimap"
}
private val context = canvas.getContext("2d") as CanvasRenderingContext2D
- init {
- document.body!!.appendChild(canvas)
- }
-
private fun drawMap() {
for (y in 0 until map.height) {
for (x in 0 until map.width) {
val wall = map.data[y][x]
if (wall > 0) {
- context.fillStyle = "#000000"
+ context.fillStyle = "#202020"
context.fillRect((x * scale).toDouble(), (y * scale).toDouble(), scale.toDouble(), scale.toDouble())
}
}
diff --git a/src/main/kotlin/RaycastContext.kt b/src/main/kotlin/RaycastContext.kt
index 808f3c4..2f8ec9c 100644
--- a/src/main/kotlin/RaycastContext.kt
+++ b/src/main/kotlin/RaycastContext.kt
@@ -1,4 +1,5 @@
data class RaycastContext(
+ val raycastOptions: RaycastOptions,
val renderer: Renderer,
val textureManager: TextureManager,
val camera: Camera,
diff --git a/src/main/kotlin/Raycaster.kt b/src/main/kotlin/Raycaster.kt
index ff2cc24..04b4d6f 100644
--- a/src/main/kotlin/Raycaster.kt
+++ b/src/main/kotlin/Raycaster.kt
@@ -1,11 +1,16 @@
import kotlin.js.Date
import kotlin.math.pow
-class Raycaster(private val stepPrecision: Int) {
+data class RaycastOptions(
+ var fixFisheye: Boolean,
+ var stepPrecision: Int
+)
+
+class Raycaster {
fun raycast(raycastContext: RaycastContext) {
val raycastStartMs = Date().getTime()
- val (renderer, textureManager, camera, map, _) = raycastContext
+ val (options, renderer, textureManager, camera, map, _) = raycastContext
val viewportWidth = renderer.viewportWidth
val viewportHeight = renderer.viewportHeight
@@ -20,8 +25,8 @@ class Raycaster(private val stepPrecision: Int) {
var objectTypeHit: Int
do {
- rayX += raySweepAngle.cosine() / stepPrecision
- rayY += raySweepAngle.sine() / stepPrecision
+ rayX += raySweepAngle.cosine() / options.stepPrecision
+ rayY += raySweepAngle.sine() / options.stepPrecision
// TODO bounds checking
objectTypeHit = map.data[rayY.toFlooredInt()][rayX.toFlooredInt()]
@@ -31,7 +36,9 @@ class Raycaster(private val stepPrecision: Int) {
val textureXIndex = ((texture.width * (rayX + rayY)) % texture.width).toFlooredInt()
var distanceToWall = kotlin.math.sqrt((camera.xPos - rayX).pow(2) + (camera.yPos - rayY).pow(2))
-// distanceToWall *= (raySweepAngle-camera.rotation).cosine()
+ if (options.fixFisheye) {
+ distanceToWall *= (raySweepAngle-camera.rotation).cosine()
+ }
val wallHeight = viewportHeightHalf / distanceToWall
// Ceiling
diff --git a/src/main/kotlin/Renderer.kt b/src/main/kotlin/Renderer.kt
index 3a7111e..49d6b2c 100644
--- a/src/main/kotlin/Renderer.kt
+++ b/src/main/kotlin/Renderer.kt
@@ -4,18 +4,14 @@ import org.w3c.dom.HTMLCanvasElement
class Renderer(val viewportWidth: Int, val viewportHeight: Int, private val outputScale: Int) {
- private val canvas = (document.createElement("canvas") as HTMLCanvasElement)
+ private val canvas = (document.getElementById("render-output") as HTMLCanvasElement)
.apply {
width = viewportWidth * outputScale
height = viewportHeight * outputScale
}
- private val context = canvas.getContext("2d") as CanvasRenderingContext2D
-
- init {
- context.scale(outputScale.toDouble(), outputScale.toDouble())
- document.body!!.appendChild(canvas)
- }
+ private val context = (canvas.getContext("2d") as CanvasRenderingContext2D)
+ .apply { scale(outputScale.toDouble(), outputScale.toDouble()) }
fun drawLine(startX: Double, startY: Double, endX: Double, endY: Double, cssColour: String = "#FF0000") {
context.strokeStyle = cssColour
diff --git a/src/main/kotlin/Ui.kt b/src/main/kotlin/Ui.kt
index e477bec..dd8b0fc 100644
--- a/src/main/kotlin/Ui.kt
+++ b/src/main/kotlin/Ui.kt
@@ -1,23 +1,71 @@
import kotlinx.browser.document
+import kotlinx.dom.removeClass
+import org.w3c.dom.HTMLElement
+import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement
-class Ui(private val textureManager: TextureManager, private val afterChange: () -> Unit) {
- private val textureSelect: HTMLSelectElement
+class Ui(private val context: RaycastContext, private val afterChange: () -> Unit) {
+
+ private val textureSelect = registerTextureSetHandler()
+
init {
- textureSelect = registerTextureSetHandler()
+ registerFovInputHandler()
+ registerRaycastPrecisionInputHandler()
+ registerMinimapToggleHandler()
+ registerFisheyeToggleHandler()
}
private fun registerTextureSetHandler(): HTMLSelectElement {
val select = document.getElementById("texture-set") as HTMLSelectElement
select.onchange = {
- textureManager.loadTextures(select.value)
+ context.textureManager.loadTextures(select.value)
afterChange()
}
-
return select
}
+ private fun registerFovInputHandler() {
+ val fov = document.getElementById("fov") as HTMLInputElement
+ val fovValue = document.getElementById("fov-value") as HTMLElement
+ fov.oninput = {
+ context.camera.fov = fov.value.toInt()
+ fovValue.textContent = fov.value
+ afterChange()
+ }
+ }
+
+ private fun registerRaycastPrecisionInputHandler() {
+ val precision = document.getElementById("raycast-precision") as HTMLInputElement
+ val precisionValue = document.getElementById("raycast-precision-value") as HTMLElement
+ precision.oninput = {
+ context.raycastOptions.stepPrecision = precision.value.toInt()
+ precisionValue.textContent = precision.value
+ afterChange()
+ }
+ }
+
+ private fun registerMinimapToggleHandler() {
+ val toggle = document.getElementById("minimap-toggle") as HTMLInputElement
+ val minimap = document.getElementById("minimap") as HTMLElement
+ toggle.onchange = {
+ minimap.hidden = !toggle.checked
+ afterChange()
+ }
+ }
+
+ private fun registerFisheyeToggleHandler() {
+ val fixFisheye = document.getElementById("fisheye-fix") as HTMLInputElement
+ fixFisheye.oninput = {
+ context.raycastOptions.fixFisheye = fixFisheye.checked
+ afterChange()
+ }
+ }
+
fun getSelectedTextureSet(): String {
return textureSelect.value
}
+
+ fun removeLoadingIndicator() {
+ document.getElementById("output-wrapper")?.removeClass("loading")
+ }
} \ No newline at end of file
diff --git a/src/main/kotlin/main.kt b/src/main/kotlin/main.kt
index bc39316..aa69ad2 100644
--- a/src/main/kotlin/main.kt
+++ b/src/main/kotlin/main.kt
@@ -1,4 +1,5 @@
fun main() {
+ val raycastOptions = RaycastOptions(fixFisheye = false, stepPrecision = 32)
val renderer = Renderer(viewportWidth = 320, viewportHeight = 240, outputScale = 3)
val textureManager = TextureManager()
val camera = Camera(
@@ -10,15 +11,15 @@ fun main() {
val map = Map()
val minimap = Minimap(map)
- val context = RaycastContext(renderer, textureManager, camera, map, minimap)
+ val context = RaycastContext(raycastOptions, renderer, textureManager, camera, map, minimap)
- val raycaster = Raycaster(stepPrecision = 32)
+ val raycaster = Raycaster()
CameraController(camera, moveSpeed = 1.0, rotateSpeed = 15) {
paint(raycaster, context)
}
- val ui = Ui(textureManager) {
+ val ui = Ui(context) {
paint(raycaster, context)
}
@@ -26,6 +27,7 @@ fun main() {
// Do an initial paint and wait for input
paint(raycaster, context)
+ ui.removeLoadingIndicator()
}
fun paint(raycaster: Raycaster, raycastContext: RaycastContext) {
diff --git a/src/main/resources/index.html b/src/main/resources/index.html
index f224a2e..9dd0d5f 100644
--- a/src/main/resources/index.html
+++ b/src/main/resources/index.html
@@ -3,20 +3,78 @@
<head>
<meta charset="UTF-8">
<title>Kotlin Raycaster</title>
- <style>
- .texture-definition {
- display: none;
- }
- #minimap {
- transform: rotate(180deg);
- }
- </style>
+ <link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre.min.css">
+ <link rel="stylesheet" href="style.css">
</head>
<body>
+ <div class="container grid-xl">
+ <div class="columns">
+ <div class="column col-xl-12">
+ <div id="output-wrapper" class="loading loading-lg">
+ <canvas id="render-output"></canvas>
+ <canvas id="minimap"></canvas>
+ </div>
+ </div>
+ <div class="column">
+ <div class="columns controls">
+ <div class="column col-12 col-xl-3">
+ <p>A simple <a href="https://en.wikipedia.org/wiki/Ray_casting">raycasting</a> engine written from scratch in Kotlin (transpiled to JS), rendered using only vertical lines drawn on an HTML5 canvas. The source is on <a href="https://github.com/jamesbarnett91/kotlin-raycaster">GitHub</a>.</p>
+ <p>The raycasting rendering method was used in early 90's 3D games, most famously <a href="https://en.wikipedia.org/wiki/Wolfenstein_3D">Wolfenstein 3D</a>.</p>
+ <p>Use WASD to move, or the arrow buttons on mobile.</p>
+ </div>
+ <div class="column col-12 col-xl-1 hide-xl">
+ <div class="divider"></div>
+ </div>
+ <div class="divider-vert show-xl"></div>
+ <div class="column col-12 col-xl-5">
+ <div class="form-group">
+ <label class="form-label label-sm" for="texture-set">Texture set</label>
+ <select id="texture-set" class="form-select select-sm">
+ <option value="wolf3d" selected>Wolfenstein 3D</option>
+ <option value="doom">Doom E1M1</option>
+ <option value="none">None</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label class="form-label label-sm" for="fov">FOV</label>
+ <div class="input-group">
+ <input id="fov" class="slider" type="range" min="5" max="360" value="90" step="5">
+ <span id="fov-value" class="input-group-addon">90</span>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="form-label label-sm" for="raycast-precision">Precision</label>
+ <div class="input-group">
+ <input id="raycast-precision" class="slider" type="range" min="1" max="150" value="64" step="1">
+ <span id="raycast-precision-value" class="input-group-addon">64</span>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="minimap-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Show minimap
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="fisheye-fix" type="checkbox">
+ <i class="form-icon"></i> Fix fisheye (requires low FOV and high precision)
+ </label>
+ </div>
+ <button id="left" class="btn">←</button>
+ <button id="up" class="btn">↑</button>
+ <button id="down" class="btn">↓</button>
+ <button id="right" class="btn">→</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
<img data-texture-id="1" src="textures/wolf3d/wall-stone-1.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
<img data-texture-id="2" src="textures/wolf3d/wall-stone-2.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
<img data-texture-id="3" src="textures/wolf3d/wall-wood-1.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
- <img data-texture-id="4" src="textures/wolf3d/wall-wood-1.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
+ <img data-texture-id="4" src="textures/wolf3d/wall-wood-1.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d"> <!-- Reuse texture 3 -->
<img data-texture-id="5" src="textures/wolf3d/wall-wood-2.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
<img data-texture-id="7" src="textures/wolf3d/wall-blue-1.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
<img data-texture-id="6" src="textures/wolf3d/wall-blue-2.png" data-width="64" data-height="64" class="texture-definition" data-set="wolf3d">
@@ -33,10 +91,7 @@
<img data-texture-id="8" src="textures/doom/wall-stone-3.png" data-width="64" data-height="64" class="texture-definition" data-set="doom">
<img data-texture-id="9" src="textures/doom/door.png" data-width="64" data-height="64" class="texture-definition" data-set="doom">
- <select id="texture-set">
- <option value="wolf3d" selected>Wolfenstein 3D</option>
- <option value="doom">Doom E1M1</option>
- </select>
+ <img data-texture-id="1" src="textures/blank.png" data-width="64" data-height="64" class="texture-definition" data-set="none">
<script src="kotlin-raycaster.js"></script>
</body>
diff --git a/src/main/resources/style.css b/src/main/resources/style.css
new file mode 100644
index 0000000..c8b7d92
--- /dev/null
+++ b/src/main/resources/style.css
@@ -0,0 +1,45 @@
+body {
+ margin: 0.5em;
+ font-size: 15px;
+}
+
+p {
+ margin: 0 0 0.8em;
+}
+
+.texture-definition {
+ display: none;
+}
+
+#minimap {
+ transform: rotate(180deg);
+ position: absolute;
+ left: 0;
+ top: 0;
+ padding: 10px;
+}
+
+#output-wrapper {
+ position: relative;
+ width: 960px;
+ height: 720px;
+}
+
+.controls {
+ /*max-width: 20em;*/
+}
+
+.slider {
+ margin-right: 0.5em;
+ width: 100%;
+}
+
+.form-switch {
+ font-size: 0.9em;
+}
+
+.btn {
+ height: 3em;
+ width: 23%;
+ touch-action: manipulation;
+} \ No newline at end of file