diff options
| author | James Barnett <noreply@jamesbarnett.xyz> | 2020-12-30 23:13:40 +0000 |
|---|---|---|
| committer | James Barnett <noreply@jamesbarnett.xyz> | 2020-12-30 23:13:40 +0000 |
| commit | 3c3322ef1a7aca3517ff94f723004fb809dec6cd (patch) | |
| tree | fd195eeedbb03cea8b25795b5e3c8fcf1a10c05c /src/main | |
| parent | c05b68f786715b20d0a9aef6538141c4227642ae (diff) | |
| download | kotlin-raycaster-3c3322ef1a7aca3517ff94f723004fb809dec6cd.tar.xz kotlin-raycaster-3c3322ef1a7aca3517ff94f723004fb809dec6cd.zip | |
Add adjustable render options. Update UI
Diffstat (limited to 'src/main')
| -rw-r--r-- | src/main/kotlin/Camera.kt | 4 | ||||
| -rw-r--r-- | src/main/kotlin/CameraController.kt | 15 | ||||
| -rw-r--r-- | src/main/kotlin/Minimap.kt | 11 | ||||
| -rw-r--r-- | src/main/kotlin/RaycastContext.kt | 1 | ||||
| -rw-r--r-- | src/main/kotlin/Raycaster.kt | 17 | ||||
| -rw-r--r-- | src/main/kotlin/Renderer.kt | 10 | ||||
| -rw-r--r-- | src/main/kotlin/Ui.kt | 58 | ||||
| -rw-r--r-- | src/main/kotlin/main.kt | 8 | ||||
| -rw-r--r-- | src/main/resources/index.html | 81 | ||||
| -rw-r--r-- | src/main/resources/style.css | 45 |
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 |