├── .gitignore ├── .vscode └── c_cpp_properties.json ├── LICENSE ├── Makefile ├── README.md ├── audio └── .gitignore ├── demo.gif ├── dmg_audio └── .gitignore ├── graphics ├── .gitignore ├── fred_side_profile.bmp ├── fred_side_profile.json ├── garden_ceiling.bmp ├── garden_ceiling.json ├── garden_floor.bmp ├── garden_floor.json ├── wall_exterior.bmp └── wall_exterior.json ├── include ├── bg3d.h ├── camera3d.h ├── mat2.h ├── mat4.h ├── sprite3d.h ├── vec3.h └── wall3d.h ├── limits.png └── src ├── bg3d.cpp ├── camera3d.cpp ├── main.cpp ├── mat2.cpp ├── mat4.cpp ├── sprite3d.cpp ├── vec3.cpp └── wall3d.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.elf 2 | *.gba 3 | *.sav 4 | build/ 5 | assets-staging/ 6 | external/ 7 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Butano", 5 | "includePath": [ 6 | "/data/code/butano/butano/include", 7 | "${workspaceFolder}/**" 8 | ], 9 | "cStandard": "gnu17", 10 | "cppStandard": "gnu++23", 11 | "defines": [], 12 | "compilerPath": "/opt/devkitpro/devkitARM/bin/arm-none-eabi-gcc", 13 | "intelliSenseMode": "linux-gcc-arm" 14 | } 15 | ], 16 | "version": 4 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Chris Lewis-Hou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------------------------------------------- 2 | # TARGET is the name of the output. 3 | # BUILD is the directory where object files & intermediate files will be placed. 4 | # LIBBUTANO is the main directory of butano library (https://github.com/GValiente/butano). 5 | # PYTHON is the path to the python interpreter. 6 | # SOURCES is a list of directories containing source code. 7 | # INCLUDES is a list of directories containing extra header files. 8 | # DATA is a list of directories containing binary data. 9 | # GRAPHICS is a list of files and directories containing files to be processed by grit. 10 | # AUDIO is a list of files and directories containing files to be processed by mmutil. 11 | # DMGAUDIO is a list of files and directories containing files to be processed by mod2gbt and s3m2gbt. 12 | # ROMTITLE is a uppercase ASCII, max 12 characters text string containing the output ROM title. 13 | # ROMCODE is a uppercase ASCII, max 4 characters text string containing the output ROM code. 14 | # USERFLAGS is a list of additional compiler flags: 15 | # Pass -flto to enable link-time optimization. 16 | # Pass -O0 or -Og to try to make debugging work. 17 | # USERCXXFLAGS is a list of additional compiler flags for C++ code only. 18 | # USERASFLAGS is a list of additional assembler flags. 19 | # USERLDFLAGS is a list of additional linker flags: 20 | # Pass -flto= to enable parallel link-time optimization. 21 | # USERLIBDIRS is a list of additional directories containing libraries. 22 | # Each libraries directory must contains include and lib subdirectories. 23 | # USERLIBS is a list of additional libraries to link with the project. 24 | # DEFAULTLIBS links standard system libraries when it is not empty. 25 | # STACKTRACE enables stack trace logging when it is not empty. 26 | # USERBUILD is a list of additional directories to remove when cleaning the project. 27 | # EXTTOOL is an optional command executed before processing audio, graphics and code files. 28 | # 29 | # All directories are specified relative to the project directory where the makefile is found. 30 | #--------------------------------------------------------------------------------------------------------------------- 31 | TARGET := $(notdir $(CURDIR)) 32 | BUILD := build 33 | LIBBUTANO := /data/code/butano/butano 34 | PYTHON := python 35 | SOURCES := src 36 | INCLUDES := include 37 | DATA := data 38 | GRAPHICS := graphics 39 | AUDIO := audio 40 | DMGAUDIO := dmg_audio 41 | ROMTITLE := sp3d 42 | ROMCODE := SP3D 43 | USERFLAGS := -DBN_CFG_SPRITES_MAX_SORT_LAYERS=64 44 | USERCXXFLAGS := 45 | USERASFLAGS := -DBN_CFG_SPRITES_MAX_SORT_LAYERS=64 46 | USERLDFLAGS := 47 | USERLIBDIRS := 48 | USERLIBS := 49 | DEFAULTLIBS := 50 | STACKTRACE := 51 | USERBUILD := external 52 | EXTTOOL := 53 | 54 | #--------------------------------------------------------------------------------------------------------------------- 55 | # Export absolute butano path: 56 | #--------------------------------------------------------------------------------------------------------------------- 57 | ifndef LIBBUTANOABS 58 | export LIBBUTANOABS := $(realpath $(LIBBUTANO)) 59 | endif 60 | 61 | #--------------------------------------------------------------------------------------------------------------------- 62 | # Include main makefile: 63 | #--------------------------------------------------------------------------------------------------------------------- 64 | include $(LIBBUTANOABS)/butano.mak 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GBA Pseudo-3D demo 2 | 3 | ![Demo](./demo.gif) 4 | 5 | This repo is a demo that illustrates one method for creating pseudo-3d graphics on the GBA, heavily inspired by the game Sonic Battle. It uses [Butano](https://github.com/GValiente/butano) which handles most of the low-level business. 6 | 7 | This code is largely based off of my team's [GBA Jam 2024](https://itch.io/jam/gbajam24) entry, [Sleep Paradox](https://staticlinkage.itch.io/sleep-paradox). 8 | 9 | ## Controls 10 | 11 | - Arrow keys - move camera 12 | - A/B - zoom in/out 13 | - L - switch between moving the camera angle and moving the camera position 14 | 15 | ## Credits 16 | 17 | I probably never would have been able to figure all this out without discovering this [Sonic Battle (GBA) Renderer Series](https://fouramgames.com/blog/sonic-battle-renderer) by 4AM Games. They also have a demo written for PC/web: https://github.com/Ohmnivore/battle. 18 | 19 | ## Re: math 20 | 21 | The math involved here relies a lot on core 3D concepts like vectors/matrices, but doesn't go much further than that. If you're looking to learn about those, I recommend [https://gamemath.com/](https://gamemath.com/). 22 | 23 | 3D math is not my strongest point, and as such there are probably some mistakes/kludges in this code. Most notably is that the camera calculations only ever really work when it is positioned 1 unit away from its target using pitch/heading values. If you try to place the camera in an arbitrary position and have it track its target, the whole thing breaks. 24 | 25 | ## Developer notes 26 | 27 | ### Sprite limits 28 | 29 | When zoomed out and viewed at lower angles, you will quickly find that sprites start to get cut off: 30 | 31 | ![](./limits.png) 32 | 33 | This is due to hitting the limit of how many affine sprites can be displayed on one line on the GBA. This can only really be avoided by limiting the amount of walls in your scenes, or controlling your camera placement so as to reduce the number on screen that are on overlapping scan lines. 34 | 35 | ### Co-ordinate space 36 | 37 | The demo uses an XYZ co-ordinate space where X is left/right, Y is forward/backward (i.e. moving further/closer into the scene), and Z is moving up/down (i.e. jumping would involve travelling along the Z axis). This makes it easier to map between 2D/3D, since an X-Y position in 3D space maps to the same X-Y co-ordinates in 2D space. 38 | 39 | We also keep the positive/negative axes consistent with Butano's co-ordinate system, i.e. (0, 0, 0) is always the center of the scene, and moving in the positive Y direction will move you closer to the default camera position. 40 | 41 | ### Scene composition 42 | 43 | A pseudo-3d scene is composed of: 44 | 45 | - Two affine tilemaps, one each for floor/ceiling. The ceiling tilemap is offset slightly higher, which creates the illusion of perspective. Since the GBA only supports two affine tilemaps at a time, this takes up your entire tilemap allocation for as long as they are both on screen. 46 | - 32x32 Affine sprites for walls, always aligned to either the XZ or YZ plane (this demo doesn't support arbitrary wall angles). 47 | - Regular sprites for characters/objects. 48 | 49 | ### Wall generation 50 | 51 | Probably the hardest problem to solve is actually figuring out how to place walls in your scene, because it will probably vary a lot depending on how your game is set up. You could potentially place walls manually if your level editor allows it, or auto-generate them from your level data. In this example, we scan through the ceiling tilemap to find edges and place walls there automatically. 52 | 53 | It takes quite a bit of storage to hold enough wall objects for the entire scene. There are probably some creative ways you could optimise this away, but in Sleep Paradox it wasn't too much of an issue. 54 | 55 | ### Sprite sorting layers 56 | 57 | We use sprite sorting layers to ensure that sprites closer to the screen show on top of sprites that are further away, but this requires a lot more layers than Butano provides by default (see the Makefile for `-DBN_CFG_SPRITES_MAX_SORT_LAYERS=64`). This may have performance implications when used in a real game, so you may need to optimise to keep the number down, e.g. by reducing the number of sprites on screen, probably walls. -------------------------------------------------------------------------------- /audio/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/audio/.gitignore -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/demo.gif -------------------------------------------------------------------------------- /dmg_audio/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/dmg_audio/.gitignore -------------------------------------------------------------------------------- /graphics/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/graphics/.gitignore -------------------------------------------------------------------------------- /graphics/fred_side_profile.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/graphics/fred_side_profile.bmp -------------------------------------------------------------------------------- /graphics/fred_side_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sprite" 3 | } 4 | -------------------------------------------------------------------------------- /graphics/garden_ceiling.bmp: -------------------------------------------------------------------------------- 1 | BMvv(  ,$6%2P4"""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"3"2#"""3""3"2#"""3"""#!"!""""#!"!"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322""##"#!##"""##"#!##""""""11""21""""11""21""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"3"2#"""3""3"2#"""3"""#!"!""""#!"!"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322""##"#!##"""##"#!##""""""11""21""""11""21""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"3"2#"""3""3"2#"""3"""#!"!""""#!"!"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322""##"#!##"""##"#!##""""""11""21""""11""21""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"3"2#"""3""3"2#"""3"""#!"!""""#!"!"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322""##"#!##"""##"#!##""""""11""21""""11""21""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#""""##"""22"#"""2""2"22"##""22"##"3""#2"32"#"#"2#32"23"3"3222"!""#2232#2"!""#33321"32"3"2#"""3"""#!"!"""!222"!!3"""""#3"#""!3"""#"!#3"232!3"""2"##"#32""22""#"22#3"##""2#2!!32""313!!!#"""#2#3""!!"""""""!!""2""3""""!!"#22#32""#!!3"23"""22#"""##""#3#""23!#122"#32"#33""#33"""23""232!#322""##"#!##""""""11""21""""""""""#"""32#2"""""33""#31#2"#3"##32""23!22"##""23""##"!#""23""#""""""""""""""""""""""""""""""#""""##"""22"#"""2""2"22"##""22"##"3""#2"32"#"#"2#32"23"3"3222"!""#2232#2"!""#33321"32"3"2#"""3"""#!"!"""!222"!!3"""""#3"#""!3"""#"!#3"232!3"""2"##"#32""22""#"22#3"##""2#2!!32""313!!!#"""#2#3""!!"""""""!!""2""3""""!!"#22#32""#!!3"23"""22#"""##""#3#""23!#122"#32"#33""#33"""23""232!#322""##"#!##""""""11""21""""""""""#"""32#2"""""33""#31#2"#3"##32""23!22"##""23""##"!#""23""#""""""""""""""""""""""""""""""#""""##"""22"#"""2""2"22"##""22"##"3""#2"32"#"#"2#32"23"3"3222"!""#2232#2"!""#33321"32"3"2#"""3"""#!"!"""!222"!!3"""""#3"#""!3"""#"!#3"232!3"""2"##"#32""22""#"22#3"##""2#2!!32""313!!!#"""#2#3""!!"""""""!!""2""3""""!!"#22#32""#!!3"23"""22#"""##""#3#""23!#122"#32"#33""#33"""23""232!#322""##"#!##""""""11""21""""""""""#"""32#2"""""33""#31#2"#3"##32""23!22"##""23""##"!#""23""#""""""""""""""""""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#"""""""""""""""#""""##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2##"""22"#"""2""2"22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""22"##""22"##"3""#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"#2"32"#"#"2#32"23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""23"3"3222"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#2232#2"!""#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"#33321"32"3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3""3"2#"""3"""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!""""#!"!"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""!222"!!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""""#3"#""!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""#"!#3"232!3"""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""2"##"#32""22""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""#"22#3"##""2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32"2#2!!32""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""313!!!#"""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!""""#2#3""!!"""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""""!!""2""3""""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32"""!!"#22#32""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""#!!3"23"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##"""22#"""##""#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#3#""23!#122"#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""#32"#33""#33"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322"""23""232!#322""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##"""##"#!##""""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""11""21""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#""""""""""#"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33"""32#2"""""33""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""#31#2"#3"##32""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23"""23!22"##""23""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#"""##"!#""23""#""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -------------------------------------------------------------------------------- /graphics/garden_ceiling.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "affine_bg" 3 | } -------------------------------------------------------------------------------- /graphics/garden_floor.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/graphics/garden_floor.bmp -------------------------------------------------------------------------------- /graphics/garden_floor.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "affine_bg" 3 | } -------------------------------------------------------------------------------- /graphics/wall_exterior.bmp: -------------------------------------------------------------------------------- 1 | BMvv(   $6%2P447:"BGM[hvF3fDF6UDDfSdDf6DF3fDF6UDDfSdDf6DFcfDF6UDDfSdDf6DF3eDF6UDDfSdDf6DF3eDFfVDDfUdDf6DF3eTFfVBFfVdDf6DFcedFfV$&fVdDf6DFceeFfVB&fVTDf6DF3eFFfVDTfVUDe6DF3eDVfVFdfUUe6DF3eDF6VUDe6UR6DF3eDFfUTDe6TR%6DESeDVfUDFU6TEUVDESeEVe5DEU6TDVUDESeEF5SDES5UCUUDE3e$E5SDUS5SSUUDC5UDE5SETS2S5UDC53TU5SETS2U5U4C53SU5SUD32!5T43553U5QTD31!533!535D3"!1!C3"3"5A"3"#1!C2!3"5$B!3"1!A#2!#"2!!3"$!2!!"1"$""!3""!"!!!!"" -------------------------------------------------------------------------------- /graphics/wall_exterior.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sprite" 3 | } 4 | -------------------------------------------------------------------------------- /include/bg3d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bn_affine_bg_ptr.h" 4 | 5 | #include "vec3.h" 6 | #include "camera3d.h" 7 | 8 | namespace sp3d { 9 | class bg3d { 10 | private: 11 | vec3 position; 12 | bn::affine_bg_ptr bg; 13 | int height; 14 | 15 | public: 16 | bg3d(const bn::affine_bg_item& bg_item, int _height = 0, int priority = 3); 17 | void update(sp3d::camera3d& camera); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /include/camera3d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bn_affine_mat_attributes.h" 4 | #include "bn_sprite_affine_mat_ptr.h" 5 | 6 | #include "vec3.h" 7 | #include "mat4.h" 8 | 9 | namespace sp3d { 10 | class camera3d { 11 | private: 12 | int pitch, heading; 13 | bn::fixed scale; 14 | vec3 position, direction, right_axis; 15 | mat4 world_transform; 16 | bn::affine_mat_attributes affine_transform_xz, affine_transform_xy, affine_transform_yz; 17 | bn::sprite_affine_mat_ptr affine_transform_ptr_xz, affine_transform_ptr_yz; 18 | 19 | void update_transform_xy(); 20 | void update_transform_xz(const vec3& up_axis); 21 | void update_transform_yz(const vec3& up_axis); 22 | 23 | public: 24 | camera3d(); 25 | 26 | int get_pitch() const; 27 | int get_heading() const; 28 | bn::fixed get_scale() const; 29 | const vec3& get_position() const; 30 | const vec3& get_direction() const; 31 | const vec3& get_right_axis() const; 32 | const mat4& get_world_transform() const; 33 | const bn::affine_mat_attributes& get_affine_transform_xy() const; 34 | const bn::affine_mat_attributes& get_affine_transform_xz() const; 35 | const bn::affine_mat_attributes& get_affine_transform_yz() const; 36 | const bn::sprite_affine_mat_ptr& get_affine_transform_ptr_xz() const; 37 | const bn::sprite_affine_mat_ptr& get_affine_transform_ptr_yz() const; 38 | 39 | void update_camera(const vec3& target, int pitch, int heading, bn::fixed scale); 40 | vec3 to_screen(const vec3& position); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /include/mat2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bn_core.h" 4 | 5 | namespace sp3d { 6 | class mat2 { 7 | public: 8 | bn::fixed_t<16> a, b, c, d; 9 | 10 | static mat2 identity; 11 | 12 | mat2() {} 13 | mat2(bn::fixed _a, bn::fixed _b, bn::fixed _c, bn::fixed _d): a(_a), b(_b), c(_c), d(_d) {} 14 | 15 | static mat2 scale(bn::fixed x, bn::fixed y); 16 | static mat2 scale_inverse(bn::fixed x, bn::fixed y); 17 | static mat2 rotate(bn::fixed degrees); 18 | static mat2 rotate_inverse(bn::fixed degrees); 19 | }; 20 | 21 | mat2 operator*(const mat2& lhs, const mat2& rhs); 22 | mat2 operator/(const mat2& lhs, const bn::fixed_t<16> rhs); 23 | mat2 operator-(const mat2& lhs); 24 | mat2 inverse(const mat2& m); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /include/mat4.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "vec3.h" 4 | 5 | namespace sp3d { 6 | class mat4 { 7 | public: 8 | vec3 a, b, c, d; 9 | 10 | static mat4 identity; 11 | 12 | mat4() {} 13 | mat4(vec3 _a, vec3 _b, vec3 _c, vec3 _d): a(_a), b(_b), c(_c), d(_d) {} 14 | }; 15 | 16 | mat4 operator*(const mat4& lhs, const mat4& rhs); 17 | vec3 operator*(const vec3& lhs, const mat4& rhs); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /include/sprite3d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bn_optional.h" 4 | #include "bn_sprite_ptr.h" 5 | #include "bn_sprite_item.h" 6 | 7 | #include "vec3.h" 8 | #include "camera3d.h" 9 | 10 | namespace sp3d { 11 | class sprite3d { 12 | protected: 13 | vec3 position; 14 | bn::optional sprite; 15 | const bn::sprite_item* sprite_item; 16 | 17 | public: 18 | sprite3d(const bn::sprite_item& _sprite_item); 19 | virtual ~sprite3d() = default; 20 | 21 | const vec3& get_position() const; 22 | void set_position(const vec3& _position); 23 | 24 | virtual void update(sp3d::camera3d& camera); 25 | }; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /include/vec3.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bn_core.h" 4 | #include "bn_fixed_point.h" 5 | 6 | namespace sp3d { 7 | class vec3 { 8 | public: 9 | bn::fixed_t<16> x, y, z; 10 | 11 | static vec3 zero, up, right, forward; 12 | 13 | constexpr vec3() {} 14 | constexpr vec3(bn::fixed _x, bn::fixed _y, bn::fixed _z) : x(_x), y(_y), z(_z) {} 15 | 16 | bn::fixed magnitude() const; 17 | bn::fixed magnitude_squared() const; 18 | bn::fixed dot(const vec3& rhs) const; 19 | vec3 cross(const vec3& rhs) const; 20 | 21 | bn::fixed_point to_point() const; 22 | }; 23 | 24 | bool operator==(const vec3& lhs, const vec3& rhs); 25 | vec3 operator+(const vec3& lhs, const vec3& rhs); 26 | vec3 operator-(const vec3& lhs, const vec3& rhs); 27 | vec3 operator*(const vec3& lhs, const bn::fixed& rhs); 28 | vec3 operator/(const vec3& lhs, const bn::fixed& rhs); 29 | vec3 operator-(const vec3& lhs); 30 | vec3 normalise(const vec3& v); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /include/wall3d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sprite3d.h" 4 | 5 | namespace sp3d { 6 | class wall3d : public sprite3d { 7 | private: 8 | vec3 facing; 9 | 10 | public: 11 | wall3d(vec3 position, vec3 facing); 12 | 13 | void update(camera3d& camera); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /limits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrislewisdev/gba-pseudo-3d/dea920eebd19b6042a8eed2c5cc2c359c7fa0ce4/limits.png -------------------------------------------------------------------------------- /src/bg3d.cpp: -------------------------------------------------------------------------------- 1 | #include "bg3d.h" 2 | 3 | #include "bn_affine_bg_item.h" 4 | 5 | namespace sp3d { 6 | bg3d::bg3d(const bn::affine_bg_item& bg_item, int _height, int priority) : 7 | bg(bg_item.create_bg(0, 0)), 8 | height(_height) 9 | { 10 | bg.set_wrapping_enabled(false); 11 | bg.set_priority(priority); 12 | } 13 | 14 | void bg3d::update(sp3d::camera3d& camera) { 15 | vec3 screen_position = camera.to_screen(position); 16 | bn::fixed perspective_offset = bn::degrees_lut_sin(camera.get_pitch()) * camera.get_scale() * -height; 17 | bg.set_position(screen_position.x, -screen_position.y + perspective_offset); 18 | bg.set_mat_attributes(camera.get_affine_transform_xy()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/camera3d.cpp: -------------------------------------------------------------------------------- 1 | #include "camera3d.h" 2 | 3 | #include "mat2.h" 4 | 5 | namespace sp3d { 6 | camera3d::camera3d() : 7 | pitch(45), 8 | scale(1), 9 | affine_transform_ptr_xz(bn::sprite_affine_mat_ptr::create(affine_transform_xz)), 10 | affine_transform_ptr_yz(bn::sprite_affine_mat_ptr::create(affine_transform_yz)) 11 | {} 12 | 13 | int camera3d::get_pitch() const { return pitch; } 14 | int camera3d::get_heading() const { return heading; } 15 | bn::fixed camera3d::get_scale() const { return scale; } 16 | 17 | const vec3& camera3d::get_position() const { return position; } 18 | const vec3& camera3d::get_direction() const { return direction; } 19 | const vec3& camera3d::get_right_axis() const { return right_axis; } 20 | 21 | const mat4& camera3d::get_world_transform() const { return world_transform; } 22 | 23 | const bn::affine_mat_attributes& camera3d::get_affine_transform_xy() const { return affine_transform_xy; } 24 | const bn::affine_mat_attributes& camera3d::get_affine_transform_xz() const { return affine_transform_xz; } 25 | const bn::affine_mat_attributes& camera3d::get_affine_transform_yz() const { return affine_transform_yz; } 26 | const bn::sprite_affine_mat_ptr& camera3d::get_affine_transform_ptr_xz() const { return affine_transform_ptr_xz; } 27 | const bn::sprite_affine_mat_ptr& camera3d::get_affine_transform_ptr_yz() const { return affine_transform_ptr_yz; } 28 | 29 | void camera3d::update_camera(const sp3d::vec3& target, int _pitch, int _heading, bn::fixed _scale) { 30 | position = vec3( 31 | target.x + -bn::degrees_lut_sin(_heading) * bn::degrees_lut_cos(_pitch), 32 | target.y + bn::degrees_lut_cos(_heading) * bn::degrees_lut_cos(_pitch), 33 | target.z + bn::degrees_lut_sin(_pitch) 34 | ); 35 | 36 | // If camera orientation hasn't changed, no need to recalculate everything else 37 | if (pitch == _pitch && scale == _scale && heading == _heading) return; 38 | 39 | pitch = _pitch; 40 | scale = _scale; 41 | heading = _heading; 42 | 43 | // We could probably recalculate all these things only when pitch/heading changes... 44 | direction = normalise(target - position); 45 | 46 | right_axis = vec3(bn::degrees_lut_cos(heading), bn::degrees_lut_sin(heading), 0); 47 | 48 | vec3 up_axis( 49 | direction.x, 50 | direction.y, 51 | bn::degrees_lut_sin(pitch) 52 | ); 53 | 54 | world_transform = mat4( 55 | vec3(right_axis.x, up_axis.x, direction.x), 56 | vec3(right_axis.y, up_axis.y, direction.y), 57 | vec3(right_axis.z, up_axis.z, direction.z), 58 | // This was supposed to be the -position vector, but my maths must be off somehow because it wasn't working correctly 59 | // Instead we just apply the translation in sprite3dcpp before transformation 60 | vec3::zero 61 | ); 62 | 63 | update_transform_xy(); 64 | update_transform_xz(up_axis); 65 | update_transform_yz(up_axis); 66 | } 67 | 68 | vec3 camera3d::to_screen(const vec3& v) { 69 | return (v - position) * world_transform * scale; 70 | } 71 | 72 | void camera3d::update_transform_xy() { 73 | auto rotation_matrix = mat2::rotate(heading); 74 | auto scale_matrix = mat2::scale_inverse(scale, scale * bn::degrees_lut_cos(pitch)); 75 | auto transform = rotation_matrix * scale_matrix; 76 | 77 | affine_transform_xy.unsafe_set_register_values( 78 | transform.a.data() >> 8, 79 | transform.b.data() >> 8, 80 | transform.c.data() >> 8, 81 | transform.d.data() >> 8 82 | ); 83 | } 84 | 85 | void camera3d::update_transform_xz(const vec3& up_axis) { 86 | auto scale_matrix = mat2::scale_inverse(scale + bn::fixed(0.05), scale); 87 | auto perspective_matrix = inverse(mat2(-right_axis.x, right_axis.z, up_axis.x, up_axis.z)); 88 | auto transform = scale_matrix * perspective_matrix; 89 | 90 | affine_transform_xz.unsafe_set_register_values( 91 | transform.a.data() >> 8, 92 | transform.b.data() >> 8, 93 | transform.c.data() >> 8, 94 | transform.d.data() >> 8 95 | ); 96 | affine_transform_ptr_xz.set_attributes(affine_transform_xz); 97 | } 98 | 99 | void camera3d::update_transform_yz(const vec3& up_axis) { 100 | auto scale_matrix = mat2::scale_inverse(scale + bn::fixed(0.05), scale); 101 | auto perspective_matrix = inverse(mat2(-right_axis.y, right_axis.z, up_axis.y, up_axis.z)); 102 | auto transform = scale_matrix * perspective_matrix; 103 | 104 | affine_transform_yz.unsafe_set_register_values( 105 | transform.a.data() >> 8, 106 | transform.b.data() >> 8, 107 | transform.c.data() >> 8, 108 | transform.d.data() >> 8 109 | ); 110 | affine_transform_ptr_yz.set_attributes(affine_transform_yz); 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "bn_core.h" 2 | #include "bn_sprite_ptr.h" 3 | #include "bn_keypad.h" 4 | #include "bn_vector.h" 5 | #include "bn_affine_bg_map_cell_info.h" 6 | 7 | #include "sprite3d.h" 8 | #include "camera3d.h" 9 | #include "bg3d.h" 10 | #include "wall3d.h" 11 | 12 | #include "bn_sprite_items_fred_side_profile.h" 13 | #include "bn_affine_bg_items_garden_floor.h" 14 | #include "bn_affine_bg_items_garden_ceiling.h" 15 | 16 | constexpr int metatile_size = 32; 17 | constexpr int tile_size = 8; 18 | constexpr int empty_tile = 0; 19 | 20 | void generate_walls(const bn::affine_bg_map_item&, bn::ivector&); 21 | 22 | int main() 23 | { 24 | bn::core::init(); 25 | 26 | // Set up the scene 27 | sp3d::camera3d camera; 28 | sp3d::sprite3d player(bn::sprite_items::fred_side_profile); 29 | sp3d::sprite3d npc(bn::sprite_items::fred_side_profile); 30 | sp3d::bg3d floor(bn::affine_bg_items::garden_floor); 31 | sp3d::bg3d ceiling(bn::affine_bg_items::garden_ceiling, 32, 1); 32 | bn::vector walls; 33 | 34 | npc.set_position(sp3d::vec3(50, 50, 16)); 35 | player.set_position(sp3d::vec3(0, 0, 16)); 36 | generate_walls(bn::affine_bg_items::garden_ceiling.map_item(), walls); 37 | 38 | // For controlling the camera 39 | bool control_position = false; 40 | int heading = 0, pitch = 30; 41 | bn::fixed scale = 1.3; 42 | sp3d::vec3 camera_position; 43 | 44 | while(true) 45 | { 46 | if (bn::keypad::l_pressed()) { 47 | control_position = !control_position; 48 | } 49 | 50 | // Camera controls 51 | if (control_position) { 52 | if (bn::keypad::left_held()) { 53 | camera_position.x -= 1; 54 | } else if (bn::keypad::right_held()) { 55 | camera_position.x += 1; 56 | } 57 | if (bn::keypad::up_held() && pitch > 0) { 58 | camera_position.y -= 1; 59 | } else if (bn::keypad::down_held() && pitch < 85) { 60 | camera_position.y += 1; 61 | } 62 | } else { 63 | if (bn::keypad::left_held()) { 64 | heading += 1; 65 | } else if (bn::keypad::right_held()) { 66 | heading -= 1; 67 | } 68 | if (bn::keypad::up_held() && pitch > 20) { 69 | pitch -= 1; 70 | } else if (bn::keypad::down_held() && pitch < 85) { 71 | pitch += 1; 72 | } 73 | 74 | // Clamp heading angle to [0..360] range 75 | if (heading < 0) heading += 360; 76 | if (heading > 360) heading -= 360; 77 | } 78 | 79 | // Zoom controls. The more you zoom out, the more likely you are to run out of sprite sort layers 80 | // and to see walls disappearing from too many sprites showing on one scanline. 81 | if (bn::keypad::a_held() && scale > bn::fixed(0.8)) { 82 | scale -= 0.05; 83 | } else if (bn::keypad::b_held() && scale < bn::fixed(1.5)) { 84 | scale += 0.05; 85 | } 86 | 87 | camera.update_camera(camera_position, pitch, heading, scale); 88 | player.update(camera); 89 | npc.update(camera); 90 | floor.update(camera); 91 | ceiling.update(camera); 92 | 93 | for (auto& wall : walls) { 94 | wall.update(camera); 95 | } 96 | 97 | bn::core::update(); 98 | } 99 | } 100 | 101 | int get_tile(const bn::affine_bg_map_item& map, int x, int y) { 102 | auto cell = map.cell(x, y); 103 | bn::affine_bg_map_cell_info cell_info(cell); 104 | return cell_info.tile_index(); 105 | } 106 | 107 | /** 108 | * Wall generation will probably vary a lot depending on your game's level data and requirements. 109 | * This is a 'simple' implementation that scans through the ceiling tilemap and places walls wherever 110 | * it finds a ceiling edge. We assume that the ceiling is aligned to a 32x32 grid rather than 8x8, 111 | * so we only need to check every 4th tile. 112 | */ 113 | void generate_walls(const bn::affine_bg_map_item& map, bn::ivector& storage) { 114 | const int map_width = map.dimensions().width(); 115 | const int map_height = map.dimensions().height(); 116 | const int half_width = (map_width * tile_size / 2); 117 | const int half_height = (map_height * tile_size / 2); 118 | 119 | for (int y = 0; y < map_height; y += 4) { 120 | for (int x = 0; x < map_width; x += 4) { 121 | int tile = get_tile(map, x, y); 122 | 123 | if (tile != empty_tile) { 124 | int left = x - 4 >= 0 ? get_tile(map, x - 4, y) : empty_tile; 125 | int right = x + 4 < map_width ? get_tile(map, x + 4, y) : empty_tile; 126 | int up = y - 4 >= 0 ? get_tile(map, x, y - 4) : empty_tile; 127 | int down = y + 4 < map_height ? get_tile(map, x, y + 4) : empty_tile; 128 | 129 | // Convert the co-ordinates from [0..512] to [-256..256], accounting for possible different tile/metatile sizes 130 | sp3d::vec3 wall_position( 131 | x * tile_size + (metatile_size / 2) - half_width, 132 | y * tile_size + (metatile_size / 2) - half_height, 133 | metatile_size / 2 134 | ); 135 | 136 | if (left == empty_tile) { 137 | sp3d::vec3 offset(-metatile_size / 2, 0, 0); 138 | storage.push_back(sp3d::wall3d(wall_position + offset, -sp3d::vec3::right)); 139 | } 140 | if (right == empty_tile) { 141 | sp3d::vec3 offset(metatile_size / 2, 0, 0); 142 | storage.push_back(sp3d::wall3d(wall_position + offset, sp3d::vec3::right)); 143 | } 144 | if (up == empty_tile) { 145 | sp3d::vec3 offset(0, -metatile_size / 2, 0); 146 | storage.push_back(sp3d::wall3d(wall_position + offset, -sp3d::vec3::forward)); 147 | } 148 | if (down == empty_tile) { 149 | sp3d::vec3 offset(0, metatile_size / 2, 0); 150 | storage.push_back(sp3d::wall3d(wall_position + offset, sp3d::vec3::forward)); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/mat2.cpp: -------------------------------------------------------------------------------- 1 | #include "mat2.h" 2 | 3 | #include "bn_math.h" 4 | 5 | namespace sp3d { 6 | mat2 mat2::identity(1, 0, 0, 1); 7 | 8 | mat2 mat2::scale(bn::fixed x, bn::fixed y) { 9 | return mat2(x, 0, 0, y); 10 | } 11 | 12 | mat2 mat2::scale_inverse(bn::fixed x, bn::fixed y) { 13 | return scale(1/x, 1/y); 14 | } 15 | 16 | mat2 mat2::rotate(bn::fixed degrees) { 17 | return mat2( 18 | bn::degrees_lut_cos(degrees), 19 | -bn::degrees_lut_sin(degrees), 20 | bn::degrees_lut_sin(degrees), 21 | bn::degrees_lut_cos(degrees) 22 | ); 23 | } 24 | 25 | mat2 mat2::rotate_inverse(bn::fixed degrees) { 26 | return rotate(360 - degrees); 27 | } 28 | 29 | mat2 operator*(const mat2& lhs, const mat2& rhs) { 30 | return mat2( 31 | lhs.a*rhs.a + lhs.b*rhs.c, 32 | lhs.a*rhs.b + lhs.b*rhs.d, 33 | lhs.c*rhs.a + lhs.d*rhs.c, 34 | lhs.c*rhs.b + lhs.d*rhs.d 35 | ); 36 | } 37 | 38 | mat2 operator/(const mat2& lhs, const bn::fixed_t<16> rhs) { 39 | return mat2(lhs.a/rhs, lhs.b/rhs, lhs.c/rhs, lhs.d/rhs); 40 | } 41 | 42 | mat2 operator-(const mat2& lhs) { 43 | return mat2(-lhs.a, -lhs.b, -lhs.c, -lhs.d); 44 | } 45 | 46 | mat2 inverse(const mat2& m) { 47 | bn::fixed denominator = m.a*m.d - m.b*m.c; 48 | 49 | // For now, it seems fine to just do this. 50 | if (denominator == 0) return mat2::identity; 51 | 52 | return mat2(m.d, -m.b, -m.c, m.a) / denominator; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/mat4.cpp: -------------------------------------------------------------------------------- 1 | #include "mat4.h" 2 | 3 | namespace sp3d { 4 | mat4 mat4::identity(vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1), vec3(0, 0, 0)); 5 | 6 | mat4 operator*(const mat4& lhs, const mat4& rhs) { 7 | vec3 a = vec3( 8 | lhs.a.x*rhs.a.x + lhs.a.y*rhs.b.x + lhs.a.z*rhs.c.x, 9 | lhs.a.x*rhs.a.y + lhs.a.y*rhs.b.y + lhs.a.z*rhs.c.y, 10 | lhs.a.x*rhs.a.z + lhs.a.y*rhs.b.z + lhs.a.z*rhs.c.z 11 | ); 12 | vec3 b = vec3( 13 | lhs.b.x*rhs.a.x + lhs.b.y*rhs.b.x + lhs.b.z*rhs.c.x, 14 | lhs.b.x*rhs.a.y + lhs.b.y*rhs.b.y + lhs.b.z*rhs.c.y, 15 | lhs.b.x*rhs.a.z + lhs.b.y*rhs.b.z + lhs.b.z*rhs.c.z 16 | ); 17 | vec3 c = vec3( 18 | lhs.c.x*rhs.a.x + lhs.c.y*rhs.b.x + lhs.c.z*rhs.c.x, 19 | lhs.c.x*rhs.a.y + lhs.c.y*rhs.b.y + lhs.c.z*rhs.c.y, 20 | lhs.c.x*rhs.a.z + lhs.c.y*rhs.b.z + lhs.c.z*rhs.c.z 21 | ); 22 | vec3 d = vec3( 23 | lhs.d.x*rhs.a.x + lhs.d.y*rhs.b.x + lhs.d.z*rhs.c.x + rhs.d.x, 24 | lhs.d.x*rhs.a.y + lhs.d.y*rhs.b.y + lhs.d.z*rhs.c.y + rhs.d.y, 25 | lhs.d.x*rhs.a.z + lhs.d.y*rhs.b.z + lhs.d.z*rhs.c.z + rhs.d.z 26 | ); 27 | 28 | return mat4(a, b, c, d); 29 | } 30 | 31 | vec3 operator*(const vec3& lhs, const mat4& rhs) { 32 | return vec3( 33 | lhs.x*rhs.a.x + lhs.y*rhs.b.x + lhs.z*rhs.c.x + rhs.d.x, 34 | lhs.x*rhs.a.y + lhs.y*rhs.b.y + lhs.z*rhs.c.y + rhs.d.y, 35 | lhs.x*rhs.a.z + lhs.y*rhs.b.z + lhs.z*rhs.c.z + rhs.d.z 36 | ); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/sprite3d.cpp: -------------------------------------------------------------------------------- 1 | #include "sprite3d.h" 2 | 3 | #include "bn_display.h" 4 | 5 | namespace sp3d { 6 | sprite3d::sprite3d(const bn::sprite_item& _sprite_item) 7 | : position(0, 16, 0), 8 | sprite_item(&_sprite_item) 9 | { 10 | } 11 | 12 | const vec3& sprite3d::get_position() const { 13 | return position; 14 | } 15 | 16 | void sprite3d::set_position(const vec3& _position) { 17 | position = _position; 18 | } 19 | 20 | void sprite3d::update(sp3d::camera3d& camera) { 21 | vec3 screen_position = camera.to_screen(position); 22 | 23 | // Check if sprite is on/off screen 24 | constexpr int clip_left = -bn::display::width() / 2 - 32; 25 | constexpr int clip_right = bn::display::width() / 2 + 32; 26 | constexpr int clip_top = bn::display::height() / 2 + 32; 27 | constexpr int clip_bottom = -bn::display::height() / 2 - 32; 28 | const bool visible = !( 29 | screen_position.x < clip_left 30 | || screen_position.x > clip_right 31 | || screen_position.y < clip_bottom 32 | || screen_position.y > clip_top 33 | ); 34 | 35 | // Butano already filters out off-screen sprites, but by using less sprite_ptrs we save on sorting layers 36 | if (!visible && sprite.has_value()) { 37 | sprite.reset(); 38 | } else if (visible && !sprite.has_value()) { 39 | sprite = sprite_item->create_sprite(0, 0); 40 | sprite->set_bg_priority(2); 41 | } 42 | 43 | if (sprite.has_value()) { 44 | sprite->set_position(screen_position.x, -screen_position.y); 45 | // Only set sort order for visible sprites to save on sorting layers 46 | if (sprite->visible()) { 47 | sprite->set_z_order(screen_position.y.integer()); 48 | } else { 49 | sprite->set_z_order(0); 50 | } 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/vec3.cpp: -------------------------------------------------------------------------------- 1 | #include "vec3.h" 2 | 3 | #include "bn_math.h" 4 | 5 | namespace sp3d { 6 | vec3 vec3::zero; 7 | vec3 vec3::right(1, 0, 0); 8 | vec3 vec3::up(0, 0, 1); 9 | vec3 vec3::forward(0, 1, 0); 10 | 11 | bn::fixed vec3::magnitude() const { 12 | return bn::sqrt(x*x + y*y + z*z); 13 | } 14 | 15 | bn::fixed vec3::magnitude_squared() const { 16 | return x*x + y*y + z*z; 17 | } 18 | 19 | bn::fixed vec3::dot(const vec3& rhs) const { 20 | return x*rhs.x + y*rhs.y + z*rhs.z; 21 | } 22 | 23 | vec3 vec3::cross(const vec3& rhs) const { 24 | return vec3(y*rhs.z - z*rhs.y, z*rhs.x - x*rhs.z, x*rhs.y - y*rhs.x); 25 | } 26 | 27 | bn::fixed_point vec3::to_point() const { 28 | return bn::fixed_point(x, z); 29 | } 30 | 31 | bool operator==(const vec3& lhs, const vec3& rhs) { 32 | return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z; 33 | } 34 | 35 | vec3 operator+(const vec3& lhs, const vec3& rhs) { 36 | return vec3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z); 37 | } 38 | 39 | vec3 operator-(const vec3& lhs, const vec3& rhs) { 40 | return vec3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z); 41 | } 42 | 43 | vec3 operator*(const vec3& lhs, const bn::fixed& rhs) { 44 | return vec3(lhs.x * rhs, lhs.y * rhs, lhs.z * rhs); 45 | } 46 | 47 | vec3 operator/(const vec3& lhs, const bn::fixed& rhs) { 48 | return vec3(lhs.x / rhs, lhs.y / rhs, lhs.z / rhs); 49 | } 50 | 51 | vec3 operator-(const vec3& lhs) { 52 | return vec3(-lhs.x, -lhs.y, -lhs.z); 53 | } 54 | 55 | vec3 normalise(const vec3& v) { 56 | return v / v.magnitude(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/wall3d.cpp: -------------------------------------------------------------------------------- 1 | #include "wall3d.h" 2 | 3 | #include "bn_sprite_items_wall_exterior.h" 4 | 5 | namespace sp3d { 6 | wall3d::wall3d(vec3 p, vec3 f) : 7 | sprite3d(bn::sprite_items::wall_exterior), 8 | facing(f) 9 | { 10 | position = p; 11 | } 12 | 13 | void wall3d::update(sp3d::camera3d& camera) { 14 | if (sprite.has_value()) { 15 | bool is_visible = facing.dot(camera.get_direction()) < 0; 16 | sprite->set_visible(is_visible); 17 | if (facing.y != 0) { 18 | sprite->set_affine_mat(camera.get_affine_transform_ptr_xz()); 19 | } else { 20 | sprite->set_affine_mat(camera.get_affine_transform_ptr_yz()); 21 | } 22 | } 23 | 24 | sprite3d::update(camera); 25 | } 26 | } 27 | --------------------------------------------------------------------------------