From 542d043057d20dfe502b272ce5e122c0065cb1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 25 Sep 2019 15:51:19 +0200 Subject: [PATCH] Init --- assets.h | 42 + assets/img2array.py | 155 ++++ assets/palette565.png | Bin 0 -> 414 bytes assets/wall_texture1.png | Bin 0 -> 1221 bytes constants.h | 14 + levels.h | 167 ++++ main.c | 207 +++++ palette.h | 66 ++ platform_sdl.h | 104 +++ raycastlib.h | 1876 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 2631 insertions(+) create mode 100644 assets.h create mode 100644 assets/img2array.py create mode 100644 assets/palette565.png create mode 100644 assets/wall_texture1.png create mode 100644 constants.h create mode 100644 levels.h create mode 100755 main.c create mode 100644 palette.h create mode 100644 platform_sdl.h create mode 100644 raycastlib.h diff --git a/assets.h b/assets.h new file mode 100644 index 0000000..491cab9 --- /dev/null +++ b/assets.h @@ -0,0 +1,42 @@ +#ifndef _SFG_RESOURCES_H +#define _SFG_RESOURCES_H + +#define SFG_TEXTURE_SIZE 32 + +static inline uint8_t SFG_getTexel(uint8_t *texture, uint8_t x, uint8_t y) +{ + return texture[(y & 0x1f) * SFG_TEXTURE_SIZE + (x & 0x1f)]; +} + +const uint8_t SFG_textureWall1[SFG_TEXTURE_SIZE * SFG_TEXTURE_SIZE] = { +5,4,4,5,59,2,5,5,4,66,45,5,4,4,4,4,4,4,2,5,4,44,4,4,4,4,3,3,2,44,4,4,4,4,4,51, +67,8,5,86,3,2,4,5,4,4,4,4,4,4,2,4,5,4,4,5,4,5,4,50,3,60,44,5,131,131,131,3,2,66, +60,4,2,2,3,3,4,3,4,3,4,3,49,2,2,5,4,3,51,3,131,2,50,66,2,131,4,4,4,4,66,3,3,3, +81,81,3,4,3,59,2,66,82,81,2,66,218,66,138,131,2,2,56,3,4,4,4,4,44,4,4,4,2,6,6,6, +5,6,4,4,6,6,5,6,5,6,6,6,6,6,5,6,5,6,6,3,4,4,4,5,4,4,4,4,3,6,5,5,5,4,4,5,5,4,5,4, +5,5,4,5,5,5,5,5,5,5,5,3,4,4,4,132,132,4,44,4,3,6,5,4,4,4,4,5,5,4,4,5,4,5,4,5,4, +5,4,5,4,5,5,2,5,4,4,4,4,4,4,3,3,6,5,5,5,4,5,5,5,5,5,5,44,5,44,5,5,5,5,5,5,5,5, +51,4,4,4,4,3,4,4,3,3,6,5,5,5,5,5,5,5,5,5,5,4,5,5,5,5,45,4,5,4,5,6,3,132,4,4,4, +66,66,2,1,3,6,4,4,5,5,5,5,5,5,5,5,4,5,4,5,4,5,5,5,4,5,5,51,4,139,138,131,3,2,2, +2,3,6,5,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,45,5,5,5,3,2,3,4,3,5,4,5,3,3,6,5,4,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,3,3,4,4,5,4,4,4,4,3,5,4,5,5,5,5,5,5,5,5,5,5,5, +5,5,5,5,5,4,5,5,5,131,3,4,4,4,4,4,4,3,3,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,5,5,5, +5,5,2,3,4,4,4,4,5,4,50,3,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,5,5,3,3,5,4,5, +4,5,4,3,3,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,2,3,4,4,5,4,4,4,137,3,6,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,2,3,4,4,4,5,4,4,0,3,6,5,28,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,6,3,3,5,5,4,3,3,3,1,3,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +5,5,4,5,5,3,50,3,3,51,132,4,3,50,3,6,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +3,1,3,3,4,4,4,4,3,3,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,28,5,4,5,4,3,5,4,4,5,4, +5,3,3,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,3,2,4,4,4,4,5,5,44,3,6,4,5,5, +5,5,5,5,5,4,5,5,4,5,5,5,5,5,4,5,4,5,3,3,5,4,4,44,5,4,4,3,5,5,4,5,5,5,6,4,5,5,5, +5,28,5,5,5,4,4,5,4,5,5,4,3,4,5,5,4,4,4,4,3,6,5,5,5,4,5,20,5,5,5,4,5,5,5,5,5,5,5, +28,5,4,5,3,3,45,4,5,4,5,5,3,3,6,5,5,5,5,5,5,5,5,5,5,5,5,5,6,5,5,5,4,5,5,6,4,3,4, +5,5,3,3,3,131,59,4,3,4,3,4,3,4,4,3,4,26,3,3,26,4,26,3,4,3,2,2,131,50,3,4,4,51,4, +4,4,4,2,50,138,217,2,2,2,2,2,2,138,3,3,2,2,81,131,3,2,3,3,4,3,4,2,131,3,5,5,4,5, +5,2,3,4,4,4,81,4,4,4,4,4,4,4,4,2,4,4,4,4,4,4,4,4,5,2,4,5,5,44,44,4,5,4,3,5,5,4, +66,4,4,3,4,4,4,4,3,4,4,4,4,4,4,4,4,4,86,2,4,45,4,4,4,4,4,4,83,5,5,4,49,4,4,4,3, +4,4,4,3,3,4,4,4,4,3,4,4,3,5,3,5,4,61,4,132,4,4,4,2,5,85,4,2,4,4,4,4,4,44,4,3,3, +5,4,4,4,4,4,4,3,6,66,4,4,4 +}; + +#endif // guard diff --git a/assets/img2array.py b/assets/img2array.py new file mode 100644 index 0000000..a1bec16 --- /dev/null +++ b/assets/img2array.py @@ -0,0 +1,155 @@ +# Python tool to convert an image to C array for small3dlib. +# +# by drummyfish +# released under CC0 1.0. + +import sys +from PIL import Image + +def printHelp(): + print("Convert image to C array for small3dlib.") + print("usage:\n") + print(" python img2array.py [-xW -yH -h -nS -pT -5] file\n") + print(" -xW set width of the output to W pixels") + print(" -yH set height of the output to H pixels") + print(" -h include header guards (for texture per file)") + print(" -nS use the name S for the texture (defaut: \"texture\")") + print(" -pT use palette from file T and indexed colors (otherwise direct colors)") + print(" -5 use 565 format instead of RGB8") + print(""); + print("by Miloslav \"drummyfish\" Ciz") + print("released under CC0 1.0") + +def rgbTo565(rgb): + return ((rgb[0] >> 3) << 11) | ((rgb[1] >> 2) << 5) | ((rgb[2] >> 3)) + +if len(sys.argv) < 2: + printHelp() + quit() + +FILENAME = "" +PALETTE = "" +USE_PALETTE = False +NAME = "texture" +GUARDS = False +OUT_WIDTH = 64 +OUT_HEIGHT = 64 +USE_565 = False + +for s in sys.argv: + if s [:2] == "-x": + OUT_WIDTH = int(s[2:]) + elif s [:2] == "-y": + OUT_HEIGHT = int(s[2:]) + elif s == "-h": + GUARDS = True + elif s[:2] == "-n": + NAME = s[2:] + elif s[:2] == "-5": + USE_565 = True + elif s[:2] == "-p": + PALETTE = s[2:] + USE_PALETTE = True + else: + FILENAME = s + +imageArray = [] +paletteColors = [] +paletteArray = [] + +image = Image.open(FILENAME).convert("RGB") +pixels = image.load() + +if USE_PALETTE > 0: + palette = Image.open(PALETTE).convert("RGB") + pixelsPal = palette.load() + + for y in range(palette.size[1]): + for x in range(palette.size[0]): + c = pixelsPal[x,y] + paletteColors.append(c) + + if USE_565: + paletteArray.append(rgbTo565(c)) + else: + paletteArray.append(c[0]) + paletteArray.append(c[1]) + paletteArray.append(c[2]) + +image2 = Image.new("RGB",(OUT_WIDTH,OUT_HEIGHT),color="white") +pixels2 = image2.load() + +for y in range(OUT_HEIGHT): + for x in range(OUT_WIDTH): + coord = ( + int(x / float(OUT_WIDTH) * image.size[0]), + int(y / float(OUT_HEIGHT) * image.size[1])) + + pixel = pixels[coord] + + if USE_PALETTE: + closestIndex = 0 + closestDiff = 1024 + + # find the index of the closest color: + + for i in range(len(paletteColors)): + c = paletteColors[i] + diff = abs(pixel[0] - c[0]) + abs(pixel[1] - c[1]) + abs(pixel[2] - c[2]) + + if diff < closestDiff: + closestIndex = i + closestDiff = diff + + imageArray.append(closestIndex) + pixels2[x,y] = paletteColors[closestIndex] + else: + if USE_565: + imageArray.append(rgbTo565(pixel)) + else: + imageArray.append(pixel[0]) + imageArray.append(pixel[1]) + imageArray.append(pixel[2]) + + pixels2[x,y] = pixel + +#----------------------- + +def printArray(array, name, sizeString, dataType="const uint8_t"): + print(dataType + " " + name + "[" + sizeString + "] = {") + arrayString = "" + + lineLen = 0 + + for v in array: + item = str(v) + "," + + lineLen += len(item) + + if lineLen > 80: + arrayString += "\n" + lineLen = len(item) + + arrayString += item + + print(arrayString[:-1]) + print("}; // " + name) + +if GUARDS: + print("#ifndef " + NAME.upper() + "_TEXTURE_H") + print("#define " + NAME.upper() + "_TEXTURE_H\n") + +if USE_PALETTE: + printArray(paletteArray,NAME + "Palette",str(len(paletteArray)),"const uint16_t" if USE_565 else "const uint8_t") + print("") + +print("#define " + NAME.upper() + "_TEXTURE_WIDTH " + str(OUT_WIDTH)) +print("#define " + NAME.upper() + "_TEXTURE_HEIGHT " + str(OUT_HEIGHT)) +print("") + +printArray(imageArray,NAME + "Texture",str(len(imageArray))) + +if GUARDS: + print("\n#endif // guard") + +image2.save(NAME + "_preview.png") diff --git a/assets/palette565.png b/assets/palette565.png new file mode 100644 index 0000000000000000000000000000000000000000..6d778dbe3eebb88fe8bcfd37dde425289cd36d6d GIT binary patch literal 414 zcmV;P0b%}$P)@jkTfPjF2g##8e7|>uqfe!*^{?~34$+C>v zcDcU*`I5Jn+rMn*;%2YBxKr3yE(IKdXX~xQyc%{%p z0~Jwt;td-&?t%apd18_xsqf5SH!UGAuicFjLm(Z}kT%7lSkRp)eBeno67r6V7L>1q zBT+(vDIG&h)fPk16+J?Vt_G7e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00b>bL_t(o!>w1XZzDMr|8;g$ zS*HLi3K(#}V1PlvfPjDl0R@Hy2MP-V3JMSQPY5`$u<*cwfCB~#3J#2_fYB+yV1Pk& z8ZbH;3^0h)1+#WMAIWZaW8%rP-}C$Uy^maM{@%RlQUd^3%J8%)U|(~r?pFW+U21GL z1(uHsWK9Bhx4)#6SlzFnOASR0xS^eMRVpu!-7frj6bgLW<{$=8+Z+IpX$n;;5K@5_ z7T;RB)L<#|!dc4v&QlK6n3kp)6m7ph=tcIpwGFnm!BuTgRXa?kbC}Bn^`*x4WeWh< zV~?^dF`LeyN;TMv|1@g-itWo5*Ww!He}nub$1d8V`gey<(H=9UQC$Lc@rp(%e2VtC zSg+S_p%t4=0e(deuKV12gI_OND@ujRCa|wLtWCgRpeqf+5?GtyIR=({KwE%_1S~HA z0C}Ns`?~;7dB6s=Z47v!tZ+Tn6{08Fx?52*3aVqMcMtZ@-S7S$^0;+p zEaE%UqMI8!(9~V-Glt)a8n9pwk>9^d7TG}I=$L!P6d7ACgkqL?S}~~x9^ffQQX4pr zA3(UXP`d(zR8XaY5Te6)>hWVC$x;TSLr_*TiaTTR@w0)gHHb(M@hbvCDzD}DSUu*Q z{;o@nVp4VFB(;H*5=&O$;WopvHi(4~9aNudv6KPLINcd4ggBI9U21=DEUn(V`ha_$ z(ZP8%mbdQ-!KsJ)GF(;cWI?C+G-iN>A9nwU7sl|?@c^IDla=ddl;J5KElyEL_)Ds@ zc;G#uib8jp^0|AVmS=r1biMnI!_@0HA~q9evC=gxkF&3DRx^qypJ(3X?6<`rUgxsf zEA&t9zn3(@hd-9rj{q>6&VZ(^9CS)Sr}XEouO;X&DRikprxY<_&?$uw0x{wigkxRn zTp)BI3`5&*@lmJZ&6A^5>{OLIH8YIWg3#E=4I;YKzO|}SjSd?jHafATYIKlsGQwIA z0+Xc-5jS-7p?+rrfPAwat#bWral&;iJBGMSFd6Z*C}cQR0o|kYN}NRa06T_vI-onJ zh4s7_tNYbk*sb_0!Sae8rrO>fPP}=s0iCoo(g7C}YWTlE zN(q}#XhonK4e=`(^@=hH#GG_OVReP-R$y71LF}E1IM5IxsRDGzsOG6)Axxqo9t6R>$(^deaoP}F!U+)UU>mn0jV8A6)lJGC-R z2ZL+G4fW~glt`*BV+GZ* jl@&bYP&?}lrMAEqZQ+dD6(V*800000NkvXXu0mjfR|O!> literal 0 HcmV?d00001 diff --git a/constants.h b/constants.h new file mode 100644 index 0000000..d45f546 --- /dev/null +++ b/constants.h @@ -0,0 +1,14 @@ +#ifndef _SFG_CONSTANTS_H +#define _SFG_CONSTANTS_H + +/** + How quickly player turns left/right, in degrees per second. +*/ +#define SFG_PLAYER_TURN_SPEED 180 + +/** + How quickly player moves, in squares per second. +*/ +#define SFG_PLAYER_MOVE_SPEED 5 + +#endif // guard diff --git a/levels.h b/levels.h new file mode 100644 index 0000000..44ac615 --- /dev/null +++ b/levels.h @@ -0,0 +1,167 @@ +#ifndef _SFG_LEVELS_H +#define _SFG_LEVELS_H + +#define SFG_MAP_SIZE 64 +#define SFG_TILE_DICTIONARY_SIZE 64 + +typedef uint16_t SFG_TileDefinition; +/**< + Defines a single game map tile. The format is following: + + MSB aaabbbbb cccddddd LSB + + aaa: ceiling texture index (from texture available on the map) + bbbb: ceiling height (1111 meaning no ceiling) ABOVE the floor + ccc: floor texture index + dddd: floor height +*/ + +typedef SFG_TileDefinition SFG_TileDictionary[SFG_TILE_DICTIONARY_SIZE]; + +/// helper macros for SFG_TileDefinition +#define SFG_TD(floorH, ceilH, floorT, ceilT)\ + ((floorH & 0x001f) |\ + ((floorT & 0x0007) << 5) |\ + ((ceilH & 0x001f) << 8) |\ + ((ceilT & 0x0007) << 13)) + +#define SFG_TILE_FLOOR_HEIGHT(tile) (tile & 0x1f) + +typedef uint8_t SFG_MapArray[SFG_MAP_SIZE * SFG_MAP_SIZE]; +/**< + Game map represented as a 2D array. Array item has this format: + + MSB aabbbbbb LSB + + aa: type of square, possible values: + 00: normal + 01: moving floor (elevator), moves from height 0 to floor height + 10: moving ceiling, moves from ceiling height to floor height + 11: door + bbbbbb: index into tile dictionary +*/ + +typedef struct +{ + SFG_TileDictionary tileDictionary; + SFG_MapArray mapArray; +} SFG_Map; + +typedef struct +{ + SFG_Map map; +} SFG_Level; + +static inline SFG_TileDefinition SFG_getMapTile(SFG_Map *map, int16_t x, int16_t y) +{ + if (x < 0 || x >= SFG_MAP_SIZE || y < 0 || y >= SFG_MAP_SIZE) + return SFG_TD(10,10,0,0); + + return map->tileDictionary[map->mapArray[y * SFG_MAP_SIZE + x] & 0x3f]; +} + +static inline uint8_t SFG_getMapTileProperties(SFG_Map *map, int16_t x, int16_t y) +{ + if (x < 0 || x >= SFG_MAP_SIZE || y < 0 || y >= SFG_MAP_SIZE) + return 0; + + return map->mapArray[y * SFG_MAP_SIZE + x] & 0xc0; +} + +static const SFG_Level SFG_level0 = +{ + .map = + { + .tileDictionary = + { + SFG_TD(0 ,0 ,0,0),SFG_TD(5 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 0 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 4 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 8 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 12 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 16 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 20 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 24 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 28 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 32 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 36 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 40 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 44 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 48 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 52 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 56 + SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0),SFG_TD(0 ,0 ,0,0), // 60 + }, + + .mapArray = + { + #define o 0 + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,1 ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,1 ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,1 ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,1 ,1 ,1 ,1 ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o ,o , + #undef o + } + } +}; + +#endif // guard diff --git a/main.c b/main.c new file mode 100755 index 0000000..337b0ec --- /dev/null +++ b/main.c @@ -0,0 +1,207 @@ +#include +#include "constants.h" +#include "levels.h" +#include "assets.h" + +#define SFG_KEY_UP 0 +#define SFG_KEY_RIGHT 1 +#define SFG_KEY_DOWN 2 +#define SFG_KEY_LEFT 3 +#define SFG_KEY_A 4 +#define SFG_KEY_B 5 +#define SFG_KEY_C 6 + +/* ============================= PORTING =================================== */ + +/* When porting, define the following in your specific platform_*.h. Also you + have to call SFG_mainLoopBody() in the platform's main loop and SFG_init() + in the platform initialization. */ + +// SFG_RESOLUTION_X #define this to screen width in pixels +// SFG_RESOLUTION_Y #define this to screen height in pixels +// SFG_FPS #define this to desired FPS + +/** Return 1 (0) if given key is pressed (not pressed). */ +int8_t SFG_keyPressed(uint8_t key); + +/** Return time in ms sice program start. */ +uint32_t SFG_getTimeMs(); + +/** Sleep (yield CPU) for specified amount of ms. This is used to relieve CPU + usage. If your platform doesn't need this or handles it in other way, this + function can do nothing. */ +void SFG_sleepMs(uint16_t timeMs); + +/** Set specified screen pixel. The function doesn't have to check whether + the coordinates are within screen. */ +static inline void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex); + +/* ========================================================================= */ + +/** + Game main loop body, call this inside the platform's specific main loop. +*/ +void SFG_mainLoopBody(); + +/** + Initializes the whole program, call this in the platform initialization. +*/ +void SFG_init(); + +#include "platform_sdl.h" + +#define SFG_MS_PER_FRAME (1000 / SFG_FPS) // ms per frame with target FPS + +#define RCL_PIXEL_FUNCTION SFG_pixelFunc +#define RCL_TEXTURE_VERTICAL_STRETCH 0 + +#include "raycastlib.h" + +RCL_Camera SFG_camera; +RCL_RayConstraints SFG_rayConstraints; + +void SFG_pixelFunc(RCL_PixelInfo *pixel) +{ + uint8_t color; + uint8_t shadow = 0; + + if (pixel->isWall) + { + color = SFG_getTexel(SFG_textureWall1,pixel->texCoords.x / 16,pixel->texCoords.y / 16); + shadow = pixel->hit.direction >> 1; + } + else + { + color = pixel->isFloor ? 20 : 50; + } + + shadow += pixel->depth / (RCL_UNITS_PER_SQUARE * 2); + + color = palette_minusValue(color,shadow); + + SFG_setPixel(pixel->position.x,pixel->position.y,color); +} + +RCL_Unit SFG_textureAt(int16_t x, int16_t y) +{ + return 0; +} + +RCL_Unit SFG_floorHeightAt(int16_t x, int16_t y) +{ + SFG_TileDefinition tile = SFG_getMapTile(&SFG_level0,x,y); + + return SFG_TILE_FLOOR_HEIGHT(tile) * (RCL_UNITS_PER_SQUARE / 2); +} + +RCL_Unit SFG_ceilingHeightAt(int16_t x, int16_t y) +{ + return RCL_UNITS_PER_SQUARE * 8; +} + +uint32_t SFG_frame; +uint32_t SFG_lastFrameTimeMs; + +void SFG_init() +{ + SFG_frame = 0; + SFG_lastFrameTimeMs = 0; + + RCL_initCamera(&SFG_camera); + RCL_initRayConstraints(&SFG_rayConstraints); + + SFG_camera.resolution.x = SFG_RESOLUTION_X; + SFG_camera.resolution.y = SFG_RESOLUTION_Y; + SFG_camera.height = RCL_UNITS_PER_SQUARE; + SFG_camera.position.x = RCL_UNITS_PER_SQUARE * 5; + SFG_camera.position.y = RCL_UNITS_PER_SQUARE * 5; + + SFG_rayConstraints.maxHits = 6; + SFG_rayConstraints.maxSteps = 32; +} + +#define SFG_PLAYER_TURN_UNITS_PER_FRAME\ + ((SFG_PLAYER_TURN_SPEED * RCL_UNITS_PER_SQUARE) / (360 * SFG_FPS)) + +#define SFG_PLAYER_MOVE_UNITS_PER_FRAME\ + ((SFG_PLAYER_MOVE_SPEED * RCL_UNITS_PER_SQUARE) / SFG_FPS) + +RCL_Vector2D playerDirection; + +/** + Performs one game step (logic, physics), happening SFG_MS_PER_FRAME after + previous frame. +*/ +void SFG_gameStep() +{ + int8_t recomputeDirection = 0; + + if (SFG_keyPressed(SFG_KEY_LEFT)) + { + SFG_camera.direction -= SFG_PLAYER_TURN_UNITS_PER_FRAME; + recomputeDirection = 1; + } + else if (SFG_keyPressed(SFG_KEY_RIGHT)) + { + SFG_camera.direction += SFG_PLAYER_TURN_UNITS_PER_FRAME; + recomputeDirection = 1; + } + + if (recomputeDirection) + { + playerDirection = RCL_angleToDirection(SFG_camera.direction); + + playerDirection.x = (playerDirection.x * SFG_PLAYER_MOVE_UNITS_PER_FRAME) / RCL_UNITS_PER_SQUARE; + playerDirection.y = (playerDirection.y * SFG_PLAYER_MOVE_UNITS_PER_FRAME) / RCL_UNITS_PER_SQUARE; + } + + if (SFG_keyPressed(SFG_KEY_UP)) + { + SFG_camera.position.x += playerDirection.x; + SFG_camera.position.y += playerDirection.y; + } + else if (SFG_keyPressed(SFG_KEY_DOWN)) + { + SFG_camera.position.x -= playerDirection.x; + SFG_camera.position.y -= playerDirection.y; + } + + if (SFG_keyPressed(SFG_KEY_A)) + SFG_camera.height += SFG_PLAYER_MOVE_UNITS_PER_FRAME; + else if (SFG_keyPressed(SFG_KEY_B)) + SFG_camera.height -= SFG_PLAYER_MOVE_UNITS_PER_FRAME; +} + +void SFG_mainLoopBody() +{ + /* standard deterministic game loop, independed on actuall achieved FPS, + each game logic (physics) frame is performed with the SFG_MS_PER_FRAME + delta time. */ + + uint32_t timeNow = SFG_getTimeMs(); + uint16_t timeSinceLastFrame = timeNow - SFG_lastFrameTimeMs; + + if (timeSinceLastFrame >= SFG_MS_PER_FRAME) + { + // perform game logic (physics), for each frame + while (timeSinceLastFrame >= SFG_MS_PER_FRAME) + { + SFG_gameStep(); + + timeSinceLastFrame -= SFG_MS_PER_FRAME; + + SFG_frame++; + } + + // render noly once + RCL_renderComplex(SFG_camera,SFG_floorHeightAt,SFG_ceilingHeightAt,SFG_textureAt,SFG_rayConstraints); + + SFG_lastFrameTimeMs = timeNow; + } + + uint32_t timeNextFrame = timeNow + SFG_MS_PER_FRAME; + timeNow = SFG_getTimeMs(); + + if (timeNextFrame > timeNow) + SFG_sleepMs((timeNextFrame - timeNow) / 2); // wait, relieve CPU +} diff --git a/palette.h b/palette.h new file mode 100644 index 0000000..d16d1eb --- /dev/null +++ b/palette.h @@ -0,0 +1,66 @@ +/* + General purpose HSV-based 256 color palette. + + Define PALETTE_FORMAT_565 to use the RGB565 palette, otherwise RGB8 is used. + + by Drummyfish, released under CC0 1.0 +*/ + +#ifndef PALETTE_256_H +#define PALETTE_256_H + +const uint16_t paletteRGB565[256] = { +0, 8484, 19017, 27501, 38034, 46518, 57051, 65535, 8354, 16709, 25096, 33450, +41805, 50192, 58546, 64853, 8386, 16805, 25224, 33642, 42061, 50480, 58898, +65269, 6402, 14853, 23304, 29706, 38157, 46608, 55058, 61429, 4354, 10757, +17160, 23562, 29965, 36368, 42770, 49141, 4355, 10758, 17161, 21516, 27920, +34323, 38678, 45049, 4323, 10759, 17163, 21519, 27923, 34327, 38683, 45055, +4292, 10632, 17004, 21296, 27668, 34008, 38300, 44671, 4260, 10568, 16908, +23216, 29524, 35864, 42172, 48479, 6308, 14664, 23052, 29360, 37716, 46104, +54460, 60767, 8355, 16710, 25098, 33453, 41809, 50196, 58552, 64859, 8257, +16546, 24836, 33093, 41382, 49672, 57929, 64170, 8353, 16738, 25124, 33509, +41894, 50248, 58633, 64970, 6401, 12802, 21252, 27653, 36102, 42504, 50953, +57322, 2305, 6658, 11012, 15365, 19718, 24072, 28425, 32746, 2306, 4612, 8967, +11273, 13580, 17934, 20240, 22515, 2307, 4615, 8971, 11279, 13587, 17943, 20251, +22527, 2180, 4392, 8652, 10864, 13076, 17304, 19516, 21727, 2116, 6312, 10508, +14672, 18868, 23064, 25180, 29375, 6212, 12456, 20748, 26960, 35252, 41496, +49756, 55999, 8258, 16549, 24840, 33099, 41390, 49681, 57940, 64183, 8192, +16384, 24576, 32768, 40960, 49152, 57344, 63488, 8320, 16640, 24960, 33312, +41632, 49952, 58304, 64576, 6400, 14848, 23296, 29696, 38144, 46592, 52992, +61408, 2304, 6656, 8960, 13312, 15616, 19968, 22272, 26592, 256, 513, 769, 1026, +1283, 1539, 1796, 2021, 258, 517, 776, 1035, 1294, 1552, 1811, 2038, 164, 360, +556, 752, 948, 1144, 1308, 1503, 36, 104, 140, 208, 244, 312, 348, 415, 2052, +4104, 8204, 10256, 14356, 16408, 18460, 22559, 6148, 14344, 20492, 28688, 34836, +43032, 51228, 57375, 8194, 16388, 24582, 32777, 40971, 49165, 57359, 63505 +}; + +/** Adds value (brightness), possibly negative, to given color (represented by + its palette index). If you know you'll only be either adding or substracting, + use plusValue() or minusValue() functions, which should be faster. */ +static inline uint8_t palette_addValue(uint8_t color, int8_t add) +{ + uint8_t newValue = color + add; + + if ((newValue >> 3) == (color >> 3)) + return newValue; + else + return add > 0 ? (color | 0x07) : 0; +} + +/** Adds a positive value (brightness) to given color (represented by its + palette index). This should be a little bit faster than addValue(). */ +static inline uint8_t palette_plusValue(uint8_t color, uint8_t plus) +{ + uint8_t newValue = color + plus; + return ((newValue >> 3) == (color >> 3)) ? newValue : (color | 0x07); +} + +/** Substracts a positive value (brightness) from given color (represented by + its palette index). This should be a little bit faster than addValue(). */ +static inline uint8_t palette_minusValue(uint8_t color, uint8_t minus) +{ + uint8_t newValue = color - minus; + return ((newValue >> 3) == (color >> 3)) ? newValue : 0; +} + +#endif //guard diff --git a/platform_sdl.h b/platform_sdl.h new file mode 100644 index 0000000..98dfb94 --- /dev/null +++ b/platform_sdl.h @@ -0,0 +1,104 @@ +#ifndef _SFG_PLATFORM_H +#define _SFG_PLATFORM_H + +#include +#include +#include +#include + +#include "palette.h" + +#define SFG_RESOLUTION_X 800 +#define SFG_RESOLUTION_Y 600 + +#define SFG_FPS 60 + +const uint8_t *sdlKeyboardState; + +uint16_t screen[SFG_RESOLUTION_X * SFG_RESOLUTION_Y]; // RGB565 format + +void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex) +{ + screen[y * SFG_RESOLUTION_X + x] = paletteRGB565[colorIndex]; +} + +uint32_t SFG_getTimeMs() +{ + return SDL_GetTicks(); +} + +void SFG_sleepMs(uint16_t timeMs) +{ + usleep(timeMs * 1000); +} + +int8_t SFG_keyPressed(uint8_t key) +{ + switch (key) + { + case SFG_KEY_UP: return sdlKeyboardState[SDL_SCANCODE_UP]; break; + case SFG_KEY_RIGHT: return sdlKeyboardState[SDL_SCANCODE_RIGHT]; break; + case SFG_KEY_DOWN: return sdlKeyboardState[SDL_SCANCODE_DOWN]; break; + case SFG_KEY_LEFT: return sdlKeyboardState[SDL_SCANCODE_LEFT]; break; + case SFG_KEY_A: return sdlKeyboardState[SDL_SCANCODE_A]; break; + case SFG_KEY_B: return sdlKeyboardState[SDL_SCANCODE_S]; break; + case SFG_KEY_C: return sdlKeyboardState[SDL_SCANCODE_D]; break; + default: return 0; break; + } +} + +int main() +{ + printf("starting\n"); + + SFG_init(); + + printf("initializing SDL\n"); + + SDL_Window *window = + SDL_CreateWindow("raycasting", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, SFG_RESOLUTION_X, SFG_RESOLUTION_Y, + SDL_WINDOW_SHOWN); + + SDL_Renderer *renderer = SDL_CreateRenderer(window,-1,0); + + SDL_Texture *texture = + SDL_CreateTexture(renderer,SDL_PIXELFORMAT_RGB565,SDL_TEXTUREACCESS_STATIC, + SFG_RESOLUTION_X,SFG_RESOLUTION_Y); + + SDL_Surface *screenSurface = SDL_GetWindowSurface(window); + + SDL_Event event; + + sdlKeyboardState = SDL_GetKeyboardState(NULL); + + int running = 1; + + while (running) + { + SDL_PumpEvents(); // updates the keyboard state + + if (sdlKeyboardState[SDL_SCANCODE_Q]) + break; + + SFG_mainLoopBody(); + + SDL_UpdateTexture(texture,NULL,screen,SFG_RESOLUTION_X * sizeof(uint16_t)); + + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer,texture,NULL,NULL); + SDL_RenderPresent(renderer); + } + + printf("freeing SDL\n"); + + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + + printf("ending\n"); + + return 0; +} + +#endif // guard diff --git a/raycastlib.h b/raycastlib.h new file mode 100644 index 0000000..cfda4f1 --- /dev/null +++ b/raycastlib.h @@ -0,0 +1,1876 @@ +#ifndef RAYCASTLIB_H +#define RAYCASTLIB_H + +/** + raycastlib (RCL) - Small C header-only raycasting library for embedded and + low performance computers, such as Arduino. Only uses integer math and stdint + standard library. + + Check the defines below to fine-tune accuracy vs performance! Don't forget + to compile with optimizations. + + Before including the library define RCL_PIXEL_FUNCTION to the name of the + function (with RCL_PixelFunction signature) that will render your pixels! + + - All public (and most private) library identifiers start with RCL_. + - Game field's bottom left corner is at [0,0]. + - X axis goes right in the ground plane. + - Y axis goes up in the ground plane. + - Height means the Z (vertical) coordinate. + - Each game square is RCL_UNITS_PER_SQUARE * RCL_UNITS_PER_SQUARE points. + - Angles are in RCL_Units, 0 means pointing right (x+) and positively rotates + clockwise. A full angle has RCL_UNITS_PER_SQUARE RCL_Units. + - Most things are normalized with RCL_UNITS_PER_SQUARE (sin, cos, vector + unit length, texture coordinates etc.). + - Screen coordinates are normal: [0,0] = top left, x goes right, y goes down. + + author: Miloslav "drummyfish" Ciz + license: CC0 1.0 + version: 0.81 +*/ + +#include + +#ifndef RCL_RAYCAST_TINY /** Turns on super efficient version of this library. + Only use if neccesarry, looks ugly. Also not done + yet. */ + #define RCL_UNITS_PER_SQUARE 1024 /**< Number of RCL_Units in a side of a + spatial square. */ + typedef int32_t RCL_Unit; /**< Smallest spatial unit, there is + RCL_UNITS_PER_SQUARE units in a square's + length. This effectively serves the purpose of + a fixed-point arithmetic. */ + #define RCL_INFINITY 2000000000 +#else + #define RCL_UNITS_PER_SQUARE 32 + typedef int16_t RCL_Unit; + #define RCL_INFINITY 30000 + #define RCL_USE_DIST_APPROX 2 +#endif + +#ifndef RCL_COMPUTE_WALL_TEXCOORDS +#define RCL_COMPUTE_WALL_TEXCOORDS 1 +#endif + +#ifndef RCL_COMPUTE_FLOOR_TEXCOORDS +#define RCL_COMPUTE_FLOOR_TEXCOORDS 0 +#endif + +#ifndef RCL_FLOOR_TEXCOORDS_HEIGHT +#define RCL_FLOOR_TEXCOORDS_HEIGHT 0 /** If RCL_COMPUTE_FLOOR_TEXCOORDS == 1, + this says for what height level the + texture coords will be computed for + (for simplicity/performance only one + level is allowed). */ +#endif + +#ifndef RCL_USE_COS_LUT +#define RCL_USE_COS_LUT 0 /**< type of look up table for cos function: + 0: none (compute) + 1: 64 items + 2: 128 items */ +#endif + +#ifndef RCL_USE_DIST_APPROX +#define RCL_USE_DIST_APPROX 0 /**< What distance approximation to use: + 0: none (compute full Euclidean distance) + 1: accurate approximation + 2: octagonal approximation (LQ) */ +#endif + +#ifndef RCL_RECTILINEAR +#define RCL_RECTILINEAR 1 /**< Whether to use rectilinear perspective (normally + used), or curvilinear perspective (fish eye). */ +#endif + +#ifndef RCL_TEXTURE_VERTICAL_STRETCH +#define RCL_TEXTURE_VERTICAL_STRETCH 1 /**< Whether textures should be + stretched to wall height (possibly + slightly slower if on). */ +#endif + +#ifndef RCL_ACCURATE_WALL_TEXTURING +#define RCL_ACCURATE_WALL_TEXTURING 0 /**< If turned on, vertical wall texture + coordinates will always be calculated + with more precise (but slower) method, + otherwise RCL_MIN_TEXTURE_STEP will be + used to decide the method. */ +#endif + +#ifndef RCL_COMPUTE_FLOOR_DEPTH +#define RCL_COMPUTE_FLOOR_DEPTH 1 /**< Whether depth should be computed for + floor pixels - turns this off if not + needed. */ +#endif + +#ifndef RCL_COMPUTE_CEILING_DEPTH +#define RCL_COMPUTE_CEILING_DEPTH 1 /**< As RCL_COMPUTE_FLOOR_DEPTH but for + ceiling. */ +#endif + +#ifndef RCL_ROLL_TEXTURE_COORDS +#define RCL_ROLL_TEXTURE_COORDS 1 /**< Says whether rolling doors should also + roll the texture coordinates along (mostly + desired for doors). */ +#endif + +#ifndef RCL_VERTICAL_FOV +#define RCL_VERTICAL_FOV (RCL_UNITS_PER_SQUARE / 2) +#endif + +#ifndef RCL_HORIZONTAL_FOV +#define RCL_HORIZONTAL_FOV (RCL_UNITS_PER_SQUARE / 4) +#endif + +#define RCL_HORIZONTAL_FOV_HALF (RCL_HORIZONTAL_FOV / 2) + +#ifndef RCL_CAMERA_COLL_RADIUS +#define RCL_CAMERA_COLL_RADIUS RCL_UNITS_PER_SQUARE / 4 +#endif + +#ifndef RCL_CAMERA_COLL_HEIGHT_BELOW +#define RCL_CAMERA_COLL_HEIGHT_BELOW RCL_UNITS_PER_SQUARE +#endif + +#ifndef RCL_CAMERA_COLL_HEIGHT_ABOVE +#define RCL_CAMERA_COLL_HEIGHT_ABOVE (RCL_UNITS_PER_SQUARE / 3) +#endif + +#ifndef RCL_CAMERA_COLL_STEP_HEIGHT +#define RCL_CAMERA_COLL_STEP_HEIGHT (RCL_UNITS_PER_SQUARE / 2) +#endif + +#ifndef RCL_MIN_TEXTURE_STEP + #if RCL_TEXTURE_VERTICAL_STRETCH == 1 + #define RCL_MIN_TEXTURE_STEP 12 /**< Specifies the minimum step in pixels + that can be used to compute texture + coordinates in a fast way. Smallet step + should be faster (but less accurate). */ + #else + #define RCL_MIN_TEXTURE_STEP 24 + #endif +#endif + +#define RCL_HORIZON_DEPTH (11 * RCL_UNITS_PER_SQUARE) /**< What depth the + horizon has (the floor + depth is only + approximated with the + help of this + constant). */ +#ifndef RCL_VERTICAL_DEPTH_MULTIPLY +#define RCL_VERTICAL_DEPTH_MULTIPLY 2 /**< Defines a multiplier of height + difference when approximating floor/ceil + depth. */ +#endif + +#define RCL_min(a,b) ((a) < (b) ? (a) : (b)) +#define RCL_max(a,b) ((a) > (b) ? (a) : (b)) +#define RCL_nonZero(v) ((v) != 0 ? (v) : 1) ///< To prevent zero divisions. + +#define RCL_logV2D(v)\ + printf("[%d,%d]\n",v.x,v.y); + +#define RCL_logRay(r){\ + printf("ray:\n");\ + printf(" start: ");\ + RCL_logV2D(r.start);\ + printf(" dir: ");\ + RCL_logV2D(r.direction);} + +#define RCL_logHitResult(h){\ + printf("hit:\n");\ + printf(" square: ");\ + RCL_logV2D(h.square);\ + printf(" pos: ");\ + RCL_logV2D(h.position);\ + printf(" dist: %d\n", h.distance);\ + printf(" dir: %d\n", h.direction);\ + printf(" texcoord: %d\n", h.textureCoord);} + +#define RCL_logPixelInfo(p){\ + printf("pixel:\n");\ + printf(" position: ");\ + RCL_logV2D(p.position);\ + printf(" texCoord: ");\ + RCL_logV2D(p.texCoords);\ + printf(" depth: %d\n", p.depth);\ + printf(" height: %d\n", p.height);\ + printf(" wall: %d\n", p.isWall);\ + printf(" hit: ");\ + RCL_logHitResult(p.hit);\ + } + +#define RCL_logCamera(c){\ + printf("camera:\n");\ + printf(" position: ");\ + RCL_logV2D(c.position);\ + printf(" height: %d\n",c.height);\ + printf(" direction: %d\n",c.direction);\ + printf(" shear: %d\n",c.shear);\ + printf(" resolution: %d x %d\n",c.resolution.x,c.resolution.y);\ + } + +/// Position in 2D space. +typedef struct +{ + RCL_Unit x; + RCL_Unit y; +} RCL_Vector2D; + +typedef struct +{ + RCL_Vector2D start; + RCL_Vector2D direction; +} RCL_Ray; + +typedef struct +{ + RCL_Unit distance; /**< Distance to the hit position, or -1 if no + collision happened. If RCL_RECTILINEAR != 0, then + the distance is perpendicular to the projection + plane (fish eye correction), otherwise it is + the straight distance to the ray start + position. */ + uint8_t direction; /**< Direction of hit. The convention for angle + units is explained above. */ + RCL_Unit textureCoord; /**< Normalized (0 to RCL_UNITS_PER_SQUARE - 1) + texture coordinate (horizontal). */ + RCL_Vector2D square; ///< Collided square coordinates. + RCL_Vector2D position; ///< Exact collision position in RCL_Units. + RCL_Unit arrayValue; /** Value returned by array function (most often + this will be the floor height). */ + RCL_Unit type; /**< Integer identifying type of square (number + returned by type function, e.g. texture + index).*/ + RCL_Unit doorRoll; ///< Holds value of door roll. +} RCL_HitResult; + +typedef struct +{ + RCL_Vector2D position; + RCL_Unit direction; + RCL_Vector2D resolution; + int16_t shear; /**< Shear offset in pixels (0 => no shear), can simulate + looking up/down. */ + RCL_Unit height; +} RCL_Camera; + +/** + Holds an information about a single rendered pixel (for a pixel function + that works as a fragment shader). +*/ +typedef struct +{ + RCL_Vector2D position; ///< On-screen position. + int8_t isWall; ///< Whether the pixel is a wall or a floor/ceiling. + int8_t isFloor; ///< Whether the pixel is floor or ceiling. + int8_t isHorizon; ///< If the pixel belongs to horizon segment. + RCL_Unit depth; ///< Corrected depth. + RCL_Unit height; ///< World height (mostly for floor). + RCL_HitResult hit; ///< Corresponding ray hit. + RCL_Vector2D texCoords; /**< Normalized (0 to RCL_UNITS_PER_SQUARE - 1) + texture coordinates. */ +} RCL_PixelInfo; + +void RCL_PIXEL_FUNCTION (RCL_PixelInfo *pixel); + +typedef struct +{ + uint16_t maxHits; + uint16_t maxSteps; +} RCL_RayConstraints; + +/** + Function used to retrieve some information about cells of the rendered scene. + It should return a characteristic of given square as an integer (e.g. square + height, texture index, ...) - between squares that return different numbers + there is considered to be a collision. + + This function should be as fast as possible as it will typically be called + very often. +*/ +typedef RCL_Unit (*RCL_ArrayFunction)(int16_t x, int16_t y); + +/** + Function that renders a single pixel at the display. It is handed an info + about the pixel it should draw. + + This function should be as fast as possible as it will typically be called + very often. +*/ +typedef void (*RCL_PixelFunction)(RCL_PixelInfo *info); + +typedef void + (*RCL_ColumnFunction)(RCL_HitResult *hits, uint16_t hitCount, uint16_t x, + RCL_Ray ray); + +/** + Simple-interface function to cast a single ray. + @return The first collision result. +*/ +RCL_HitResult RCL_castRay(RCL_Ray ray, RCL_ArrayFunction arrayFunc); + +/** + Maps a single point in the world to the screen (2D position + depth). +*/ +RCL_PixelInfo RCL_mapToScreen(RCL_Vector2D worldPosition, RCL_Unit height, + RCL_Camera camera); + +/** + Casts a single ray and returns a list of collisions. + + @param ray ray to be cast, if RCL_RECTILINEAR != 0 then the computed hit + distance is divided by the ray direction vector length (to correct + the fish eye effect) + @param arrayFunc function that will be used to determine collisions (hits) + with the ray (squares for which this function returns different values + are considered to have a collision between them), this will typically + be a function returning floor height + @param typeFunc optional (can be 0) function - if provided, it will be used + to mark the hit result with the number returned by this function + (it can be e.g. a texture index) + @param hitResults array in which the hit results will be stored (has to be + preallocated with at space for at least as many hit results as + maxHits specified with the constraints parameter) + @param hitResultsLen in this variable the number of hit results will be + returned + @param constraints specifies constraints for the ray cast +*/ +void RCL_castRayMultiHit(RCL_Ray ray, RCL_ArrayFunction arrayFunc, + RCL_ArrayFunction typeFunc, RCL_HitResult *hitResults, + uint16_t *hitResultsLen, RCL_RayConstraints constraints); + +RCL_Vector2D RCL_angleToDirection(RCL_Unit angle); + +/** +Cos function. + +@param input to cos in RCL_Units (RCL_UNITS_PER_SQUARE = 2 * pi = 360 degrees) +@return RCL_normalized output in RCL_Units (from -RCL_UNITS_PER_SQUARE to + RCL_UNITS_PER_SQUARE) +*/ +RCL_Unit RCL_cosInt(RCL_Unit input); + +RCL_Unit RCL_sinInt(RCL_Unit input); + +/// Normalizes given vector to have RCL_UNITS_PER_SQUARE length. +RCL_Vector2D RCL_normalize(RCL_Vector2D v); + +/// Computes a cos of an angle between two vectors. +RCL_Unit RCL_vectorsAngleCos(RCL_Vector2D v1, RCL_Vector2D v2); + +uint16_t RCL_sqrtInt(RCL_Unit value); +RCL_Unit RCL_dist(RCL_Vector2D p1, RCL_Vector2D p2); +RCL_Unit RCL_len(RCL_Vector2D v); + +/** + Converts an angle in whole degrees to an angle in RCL_Units that this library + uses. +*/ +RCL_Unit RCL_degreesToUnitsAngle(int16_t degrees); + +///< Computes the change in size of an object due to perspective. +RCL_Unit RCL_perspectiveScale(RCL_Unit originalSize, RCL_Unit distance); + +RCL_Unit RCL_perspectiveScaleInverse(RCL_Unit originalSize, + RCL_Unit scaledSize); + +/** + Casts rays for given camera view and for each hit calls a user provided + function. +*/ +void RCL_castRaysMultiHit(RCL_Camera cam, RCL_ArrayFunction arrayFunc, + RCL_ArrayFunction typeFunction, RCL_ColumnFunction columnFunc, + RCL_RayConstraints constraints); + +/** + Using provided functions, renders a complete complex (multilevel) camera + view. + + This function should render each screen pixel exactly once. + + function rendering summary: + - performance: slower + - accuracy: higher + - wall textures: yes + - different wall heights: yes + - floor/ceiling textures: no + - floor geometry: yes, multilevel + - ceiling geometry: yes (optional), multilevel + - rolling door: no + - camera shearing: yes + - rendering order: left-to-right, not specifically ordered vertically + + @param cam camera whose view to render + @param floorHeightFunc function that returns floor height (in RCL_Units) + @param ceilingHeightFunc same as floorHeightFunc but for ceiling, can also be + 0 (no ceiling will be rendered) + @param typeFunction function that says a type of square (e.g. its texture + index), can be 0 (no type in hit result) + @param pixelFunc callback function to draw a single pixel on screen + @param constraints constraints for each cast ray +*/ +void RCL_renderComplex(RCL_Camera cam, RCL_ArrayFunction floorHeightFunc, + RCL_ArrayFunction ceilingHeightFunc, RCL_ArrayFunction typeFunction, + RCL_RayConstraints constraints); + +/** + Renders given camera view, with help of provided functions. This function is + simpler and faster than RCL_renderComplex(...) and is meant to be rendering + flat levels. + + function rendering summary: + - performance: faster + - accuracy: lower + - wall textures: yes + - different wall heights: yes + - floor/ceiling textures: yes (only floor, you can mirror it for ceiling) + - floor geometry: no (just flat floor, with depth information) + - ceiling geometry: no (just flat ceiling, with depth information) + - rolling door: yes + - camera shearing: no + - rendering order: left-to-right, top-to-bottom + + Additionally this function supports rendering rolling doors. + + This function should render each screen pixel exactly once. + + @param rollFunc function that for given square says its door roll in + RCL_Units (0 = no roll, RCL_UNITS_PER_SQUARE = full roll right, + -RCL_UNITS_PER_SQUARE = full roll left), can be zero (no rolling door, + rendering should also be faster as fewer intersections will be tested) +*/ +void RCL_renderSimple(RCL_Camera cam, RCL_ArrayFunction floorHeightFunc, + RCL_ArrayFunction typeFunc, RCL_ArrayFunction rollFunc, + RCL_RayConstraints constraints); + +/** + Function that moves given camera and makes it collide with walls and + potentially also floor and ceilings. It's meant to help implement player + movement. + + @param camera camera to move + @param planeOffset offset to move the camera in + @param heightOffset height offset to move the camera in + @param floorHeightFunc function used to retrieve the floor height + @param ceilingHeightFunc function for retrieving ceiling height, can be 0 + (camera won't collide with ceiling) + @param computeHeight whether to compute height - if false (0), floor and + ceiling functions won't be used and the camera will + only collide horizontally with walls (good for simpler + game, also faster) + @param force if true, forces to recompute collision even if position doesn't + change +*/ +void RCL_moveCameraWithCollision(RCL_Camera *camera, RCL_Vector2D planeOffset, + RCL_Unit heightOffset, RCL_ArrayFunction floorHeightFunc, + RCL_ArrayFunction ceilingHeightFunc, int8_t computeHeight, int8_t force); + +void RCL_initCamera(RCL_Camera *camera); +void RCL_initRayConstraints(RCL_RayConstraints *constraints); + +//============================================================================= +// privates + +// global helper variables, for precomputing stuff etc. +RCL_Camera _RCL_camera; +RCL_Unit _RCL_horizontalDepthStep = 0; +RCL_Unit _RCL_startFloorHeight = 0; +RCL_Unit _RCL_startCeil_Height = 0; +RCL_Unit _RCL_camResYLimit = 0; +RCL_Unit _RCL_middleRow = 0; +RCL_ArrayFunction _RCL_floorFunction = 0; +RCL_ArrayFunction _RCL_ceilFunction = 0; +RCL_Unit _RCL_fHorizontalDepthStart = 0; +RCL_Unit _RCL_cHorizontalDepthStart = 0; +int16_t _RCL_cameraHeightScreen = 0; +RCL_ArrayFunction _RCL_rollFunction = 0; // says door rolling +RCL_Unit *_RCL_floorPixelDistances = 0; + +#ifdef RCL_PROFILE + // function call counters for profiling + uint32_t profile_RCL_sqrtInt = 0; + uint32_t profile_RCL_clamp = 0; + uint32_t profile_RCL_cosInt = 0; + uint32_t profile_RCL_angleToDirection = 0; + uint32_t profile_RCL_dist = 0; + uint32_t profile_RCL_len = 0; + uint32_t profile_RCL_pointIfLeftOfRay = 0; + uint32_t profile_RCL_castRayMultiHit = 0; + uint32_t profile_RCL_castRay = 0; + uint32_t profile_RCL_absVal = 0; + uint32_t profile_RCL_normalize = 0; + uint32_t profile_RCL_vectorsAngleCos = 0; + uint32_t profile_RCL_perspectiveScale = 0; + uint32_t profile_RCL_wrap = 0; + uint32_t profile_RCL_divRoundDown = 0; + #define RCL_profileCall(c) profile_##c += 1 + + #define printProfile() {\ + printf("profile:\n");\ + printf(" RCL_sqrtInt: %d\n",profile_RCL_sqrtInt);\ + printf(" RCL_clamp: %d\n",profile_RCL_clamp);\ + printf(" RCL_cosInt: %d\n",profile_RCL_cosInt);\ + printf(" RCL_angleToDirection: %d\n",profile_RCL_angleToDirection);\ + printf(" RCL_dist: %d\n",profile_RCL_dist);\ + printf(" RCL_len: %d\n",profile_RCL_len);\ + printf(" RCL_pointIfLeftOfRay: %d\n",profile_RCL_pointIfLeftOfRay);\ + printf(" RCL_castRayMultiHit : %d\n",profile_RCL_castRayMultiHit);\ + printf(" RCL_castRay: %d\n",profile_RCL_castRay);\ + printf(" RCL_normalize: %d\n",profile_RCL_normalize);\ + printf(" RCL_vectorsAngleCos: %d\n",profile_RCL_vectorsAngleCos);\ + printf(" RCL_absVal: %d\n",profile_RCL_absVal);\ + printf(" RCL_perspectiveScale: %d\n",profile_RCL_perspectiveScale);\ + printf(" RCL_wrap: %d\n",profile_RCL_wrap);\ + printf(" RCL_divRoundDown: %d\n",profile_RCL_divRoundDown); } +#else + #define RCL_profileCall(c) +#endif + +RCL_Unit RCL_clamp(RCL_Unit value, RCL_Unit valueMin, RCL_Unit valueMax) +{ + RCL_profileCall(RCL_clamp); + + if (value >= valueMin) + { + if (value <= valueMax) + return value; + else + return valueMax; + } + else + return valueMin; +} + +static inline RCL_Unit RCL_absVal(RCL_Unit value) +{ + RCL_profileCall(RCL_absVal); + + return value >= 0 ? value : -1 * value; +} + +/// Like mod, but behaves differently for negative values. +static inline RCL_Unit RCL_wrap(RCL_Unit value, RCL_Unit mod) +{ + RCL_profileCall(RCL_wrap); + + return value >= 0 ? (value % mod) : (mod + (value % mod) - 1); +} + +/// Performs division, rounding down, NOT towards zero. +static inline RCL_Unit RCL_divRoundDown(RCL_Unit value, RCL_Unit divisor) +{ + RCL_profileCall(RCL_divRoundDown); + + return value / divisor - ((value >= 0) ? 0 : 1); +} + +// Bhaskara's cosine approximation formula +#define trigHelper(x) (((RCL_Unit) RCL_UNITS_PER_SQUARE) *\ + (RCL_UNITS_PER_SQUARE / 2 * RCL_UNITS_PER_SQUARE / 2 - 4 * (x) * (x)) /\ + (RCL_UNITS_PER_SQUARE / 2 * RCL_UNITS_PER_SQUARE / 2 + (x) * (x))) + +#if RCL_USE_COS_LUT == 1 + + #ifdef RCL_RAYCAST_TINY + const RCL_Unit cosLUT[64] = + { + 16,14,11,6,0,-6,-11,-14,-15,-14,-11,-6,0,6,11,14 + }; + #else + const RCL_Unit cosLUT[64] = + { + 1024,1019,1004,979,946,903,851,791,724,649,568,482,391,297,199,100,0,-100, + -199,-297,-391,-482,-568,-649,-724,-791,-851,-903,-946,-979,-1004,-1019, + -1023,-1019,-1004,-979,-946,-903,-851,-791,-724,-649,-568,-482,-391,-297, + -199,-100,0,100,199,297,391,482,568,649,724,791,851,903,946,979,1004,1019 + }; + #endif + +#elif RCL_USE_COS_LUT == 2 +const RCL_Unit cosLUT[128] = +{ + 1024,1022,1019,1012,1004,993,979,964,946,925,903,878,851,822,791,758,724, + 687,649,609,568,526,482,437,391,344,297,248,199,150,100,50,0,-50,-100,-150, + -199,-248,-297,-344,-391,-437,-482,-526,-568,-609,-649,-687,-724,-758,-791, + -822,-851,-878,-903,-925,-946,-964,-979,-993,-1004,-1012,-1019,-1022,-1023, + -1022,-1019,-1012,-1004,-993,-979,-964,-946,-925,-903,-878,-851,-822,-791, + -758,-724,-687,-649,-609,-568,-526,-482,-437,-391,-344,-297,-248,-199,-150, + -100,-50,0,50,100,150,199,248,297,344,391,437,482,526,568,609,649,687,724, + 758,791,822,851,878,903,925,946,964,979,993,1004,1012,1019,1022 +}; +#endif + +RCL_Unit RCL_cosInt(RCL_Unit input) +{ + RCL_profileCall(RCL_cosInt); + + input = RCL_wrap(input,RCL_UNITS_PER_SQUARE); + +#if RCL_USE_COS_LUT == 1 + + #ifdef RCL_RAYCAST_TINY + return cosLUT[input]; + #else + return cosLUT[input / 16]; + #endif + +#elif RCL_USE_COS_LUT == 2 + return cosLUT[input / 8]; +#else + if (input < RCL_UNITS_PER_SQUARE / 4) + return trigHelper(input); + else if (input < RCL_UNITS_PER_SQUARE / 2) + return -1 * trigHelper(RCL_UNITS_PER_SQUARE / 2 - input); + else if (input < 3 * RCL_UNITS_PER_SQUARE / 4) + return -1 * trigHelper(input - RCL_UNITS_PER_SQUARE / 2); + else + return trigHelper(RCL_UNITS_PER_SQUARE - input); +#endif +} + +#undef trigHelper + +RCL_Unit RCL_sinInt(RCL_Unit input) +{ + return RCL_cosInt(input - RCL_UNITS_PER_SQUARE / 4); +} + +RCL_Vector2D RCL_angleToDirection(RCL_Unit angle) +{ + RCL_profileCall(RCL_angleToDirection); + + RCL_Vector2D result; + + result.x = RCL_cosInt(angle); + result.y = -1 * RCL_sinInt(angle); + + return result; +} + +uint16_t RCL_sqrtInt(RCL_Unit value) +{ + RCL_profileCall(RCL_sqrtInt); + +#ifdef RCL_RAYCAST_TINY + uint16_t result = 0; + uint16_t a = value; + uint16_t b = 1u << 14; +#else + uint32_t result = 0; + uint32_t a = value; + uint32_t b = 1u << 30; +#endif + + while (b > a) + b >>= 2; + + while (b != 0) + { + if (a >= result + b) + { + a -= result + b; + result = result + 2 * b; + } + + b >>= 2; + result >>= 1; + } + + return result; +} + +RCL_Unit RCL_dist(RCL_Vector2D p1, RCL_Vector2D p2) +{ + RCL_profileCall(RCL_dist); + + RCL_Unit dx = p2.x - p1.x; + RCL_Unit dy = p2.y - p1.y; + +#if RCL_USE_DIST_APPROX == 2 + // octagonal approximation + + dx = RCL_absVal(dx); + dy = RCL_absVal(dy); + + return dy > dx ? dx / 2 + dy : dy / 2 + dx; +#elif RCL_USE_DIST_APPROX == 1 + // more accurate approximation + + RCL_Unit a, b, result; + + dx = dx < 0 ? -1 * dx : dx; + dy = dy < 0 ? -1 * dy : dy; + + if (dx < dy) + { + a = dy; + b = dx; + } + else + { + a = dx; + b = dy; + } + + result = a + (44 * b) / 102; + + if (a < (b << 4)) + result -= (5 * a) / 128; + + return result; +#else + dx = dx * dx; + dy = dy * dy; + + return RCL_sqrtInt((RCL_Unit) (dx + dy)); +#endif +} + +RCL_Unit RCL_len(RCL_Vector2D v) +{ + RCL_profileCall(RCL_len); + + RCL_Vector2D zero; + zero.x = 0; + zero.y = 0; + + return RCL_dist(zero,v); +} + +static inline int8_t RCL_pointIfLeftOfRay(RCL_Vector2D point, RCL_Ray ray) +{ + RCL_profileCall(RCL_pointIfLeftOfRay); + + RCL_Unit dX = point.x - ray.start.x; + RCL_Unit dY = point.y - ray.start.y; + return (ray.direction.x * dY - ray.direction.y * dX) > 0; + // ^ Z component of cross-product +} + +void RCL_castRayMultiHit(RCL_Ray ray, RCL_ArrayFunction arrayFunc, + RCL_ArrayFunction typeFunc, RCL_HitResult *hitResults, + uint16_t *hitResultsLen, RCL_RayConstraints constraints) +{ + RCL_profileCall(RCL_castRayMultiHit); + + RCL_Vector2D currentPos = ray.start; + RCL_Vector2D currentSquare; + + currentSquare.x = RCL_divRoundDown(ray.start.x,RCL_UNITS_PER_SQUARE); + currentSquare.y = RCL_divRoundDown(ray.start.y,RCL_UNITS_PER_SQUARE); + + *hitResultsLen = 0; + + RCL_Unit squareType = arrayFunc(currentSquare.x,currentSquare.y); + + // DDA variables + RCL_Vector2D nextSideDist; // dist. from start to the next side in given axis + RCL_Vector2D delta; + RCL_Vector2D step; // -1 or 1 for each axis + int8_t stepHorizontal = 0; // whether the last step was hor. or vert. + + nextSideDist.x = 0; + nextSideDist.y = 0; + + RCL_Unit dirVecLengthNorm = RCL_len(ray.direction) * RCL_UNITS_PER_SQUARE; + + delta.x = RCL_absVal(dirVecLengthNorm / RCL_nonZero(ray.direction.x)); + delta.y = RCL_absVal(dirVecLengthNorm / RCL_nonZero(ray.direction.y)); + + // init DDA + + if (ray.direction.x < 0) + { + step.x = -1; + nextSideDist.x = (RCL_wrap(ray.start.x,RCL_UNITS_PER_SQUARE) * delta.x) / + RCL_UNITS_PER_SQUARE; + } + else + { + step.x = 1; + nextSideDist.x = + ((RCL_wrap(RCL_UNITS_PER_SQUARE - ray.start.x,RCL_UNITS_PER_SQUARE)) * + delta.x) / RCL_UNITS_PER_SQUARE; + } + + if (ray.direction.y < 0) + { + step.y = -1; + nextSideDist.y = (RCL_wrap(ray.start.y,RCL_UNITS_PER_SQUARE) * delta.y) / + RCL_UNITS_PER_SQUARE; + } + else + { + step.y = 1; + nextSideDist.y = + ((RCL_wrap(RCL_UNITS_PER_SQUARE - ray.start.y,RCL_UNITS_PER_SQUARE)) * + delta.y) / RCL_UNITS_PER_SQUARE; + } + + // DDA loop + + for (uint16_t i = 0; i < constraints.maxSteps; ++i) + { + RCL_Unit currentType = arrayFunc(currentSquare.x,currentSquare.y); + + if (currentType != squareType) + { + // collision + + RCL_HitResult h; + + h.arrayValue = currentType; + h.doorRoll = 0; + h.position = currentPos; + h.square = currentSquare; + + if (stepHorizontal) + { + h.position.x = currentSquare.x * RCL_UNITS_PER_SQUARE; + h.direction = 3; + + if (step.x == -1) + { + h.direction = 1; + h.position.x += RCL_UNITS_PER_SQUARE; + } + + RCL_Unit diff = h.position.x - ray.start.x; + h.position.y = ray.start.y + ((ray.direction.y * diff) / + RCL_nonZero(ray.direction.x)); + +#if RCL_RECTILINEAR + /* Here we compute the fish eye corrected distance (perpendicular to + the projection plane) as the Euclidean distance divided by the length + of the ray direction vector. This can be computed without actually + computing Euclidean distances as a hypothenuse A (distance) divided + by hypothenuse B (length) is equal to leg A (distance along one axis) + divided by leg B (length along the same axis). */ + + h.distance = + ((h.position.x - ray.start.x) * RCL_UNITS_PER_SQUARE) / + RCL_nonZero(ray.direction.x); +#endif + } + else + { + h.position.y = currentSquare.y * RCL_UNITS_PER_SQUARE; + h.direction = 2; + + if (step.y == -1) + { + h.direction = 0; + h.position.y += RCL_UNITS_PER_SQUARE; + } + + RCL_Unit diff = h.position.y - ray.start.y; + h.position.x = ray.start.x + ((ray.direction.x * diff) / + RCL_nonZero(ray.direction.y)); + +#if RCL_RECTILINEAR + h.distance = + ((h.position.y - ray.start.y) * RCL_UNITS_PER_SQUARE) / + RCL_nonZero(ray.direction.y); +#endif + } + +#if !RCL_RECTILINEAR + h.distance = RCL_dist(h.position,ray.start); +#endif + + if (typeFunc != 0) + h.type = typeFunc(currentSquare.x,currentSquare.y); + +#if RCL_COMPUTE_WALL_TEXCOORDS == 1 + switch (h.direction) + { + case 0: h.textureCoord = + RCL_wrap(-1 * h.position.x,RCL_UNITS_PER_SQUARE); break; + + case 1: h.textureCoord = + RCL_wrap(h.position.y,RCL_UNITS_PER_SQUARE); break; + + case 2: h.textureCoord = + RCL_wrap(h.position.x,RCL_UNITS_PER_SQUARE); break; + + case 3: h.textureCoord = + RCL_wrap(-1 * h.position.y,RCL_UNITS_PER_SQUARE); break; + + default: h.textureCoord = 0; break; + } + + if (_RCL_rollFunction != 0) + { + h.doorRoll = _RCL_rollFunction(currentSquare.x,currentSquare.y); + + if (h.direction == 0 || h.direction == 1) + h.doorRoll *= -1; + } + +#else + h.textureCoord = 0; +#endif + + hitResults[*hitResultsLen] = h; + + *hitResultsLen += 1; + + squareType = currentType; + + if (*hitResultsLen >= constraints.maxHits) + break; + } + + // DDA step + + if (nextSideDist.x < nextSideDist.y) + { + nextSideDist.x += delta.x; + currentSquare.x += step.x; + stepHorizontal = 1; + } + else + { + nextSideDist.y += delta.y; + currentSquare.y += step.y; + stepHorizontal = 0; + } + } +} + +RCL_HitResult RCL_castRay(RCL_Ray ray, RCL_ArrayFunction arrayFunc) +{ + RCL_profileCall(RCL_castRay); + + RCL_HitResult result; + uint16_t RCL_len; + RCL_RayConstraints c; + + c.maxSteps = 1000; + c.maxHits = 1; + + RCL_castRayMultiHit(ray,arrayFunc,0,&result,&RCL_len,c); + + if (RCL_len == 0) + result.distance = -1; + + return result; +} + +void RCL_castRaysMultiHit(RCL_Camera cam, RCL_ArrayFunction arrayFunc, + RCL_ArrayFunction typeFunction, RCL_ColumnFunction columnFunc, + RCL_RayConstraints constraints) +{ + RCL_Vector2D dir1 = + RCL_angleToDirection(cam.direction - RCL_HORIZONTAL_FOV_HALF); + + RCL_Vector2D dir2 = + RCL_angleToDirection(cam.direction + RCL_HORIZONTAL_FOV_HALF); + + RCL_Unit dX = dir2.x - dir1.x; + RCL_Unit dY = dir2.y - dir1.y; + + RCL_HitResult hits[constraints.maxHits]; + uint16_t hitCount; + + RCL_Ray r; + r.start = cam.position; + + RCL_Unit currentDX = 0; + RCL_Unit currentDY = 0; + + for (int16_t i = 0; i < cam.resolution.x; ++i) + { + /* Here by linearly interpolating the direction vector its length changes, + which in result achieves correcting the fish eye effect (computing + perpendicular distance). */ + + r.direction.x = dir1.x + currentDX / cam.resolution.x; + r.direction.y = dir1.y + currentDY / cam.resolution.x; + + RCL_castRayMultiHit(r,arrayFunc,typeFunction,hits,&hitCount,constraints); + + columnFunc(hits,hitCount,i,r); + + currentDX += dX; + currentDY += dY; + } +} + +/** + Helper function that determines intersection with both ceiling and floor. +*/ +RCL_Unit _RCL_floorCeilFunction(int16_t x, int16_t y) +{ + RCL_Unit f = _RCL_floorFunction(x,y); + + if (_RCL_ceilFunction == 0) + return f; + + RCL_Unit c = _RCL_ceilFunction(x,y); + +#ifndef RCL_RAYCAST_TINY + return ((f & 0x0000ffff) << 16) | (c & 0x0000ffff); +#else + return ((f & 0x00ff) << 8) | (c & 0x00ff); +#endif +} + +RCL_Unit _floorHeightNotZeroFunction(int16_t x, int16_t y) +{ + return _RCL_floorFunction(x,y) == 0 ? 0 : + RCL_nonZero((x & 0x00FF) | ((y & 0x00FF) << 8)); + // ^ this makes collisions between all squares - needed for rolling doors +} + +RCL_Unit RCL_adjustDistance(RCL_Unit distance, RCL_Camera *camera, + RCL_Ray *ray) +{ + /* FIXME/TODO: The adjusted (=orthogonal, camera-space) distance could + possibly be computed more efficiently by not computing Euclidean + distance at all, but rather compute the distance of the collision + point from the projection plane (line). */ + + RCL_Unit result = + (distance * + RCL_vectorsAngleCos(RCL_angleToDirection(camera->direction), + ray->direction)) / RCL_UNITS_PER_SQUARE; + + return RCL_nonZero(result); + // ^ prevent division by zero +} + +/// Helper for drawing floor or ceiling. Returns the last drawn pixel position. +static inline int16_t _RCL_drawHorizontal( + RCL_Unit yCurrent, + RCL_Unit yTo, + RCL_Unit limit1, // TODO: int16_t? + RCL_Unit limit2, + RCL_Unit verticalOffset, + int16_t increment, + int8_t computeDepth, + int8_t computeCoords, + int16_t depthIncrementMultiplier, + RCL_Ray *ray, + RCL_PixelInfo *pixelInfo +) +{ + RCL_Unit depthIncrement; + RCL_Unit dx; + RCL_Unit dy; + + pixelInfo->isWall = 0; + + int16_t limit = RCL_clamp(yTo,limit1,limit2); + + /* for performance reasons have different version of the critical loop + to be able to branch early */ + #define loop(doDepth,doCoords)\ + {\ + if (doDepth) /*constant condition - compiler should optimize it out*/\ + {\ + pixelInfo->depth += RCL_absVal(verticalOffset) *\ + RCL_VERTICAL_DEPTH_MULTIPLY;\ + depthIncrement = depthIncrementMultiplier *\ + _RCL_horizontalDepthStep;\ + }\ + if (doCoords) /*constant condition - compiler should optimize it out*/\ + {\ + dx = pixelInfo->hit.position.x - _RCL_camera.position.x;\ + dy = pixelInfo->hit.position.y - _RCL_camera.position.y;\ + }\ + for (int16_t i = yCurrent + increment;\ + increment == -1 ? i >= limit : i <= limit; /* TODO: is efficient? */\ + i += increment)\ + {\ + pixelInfo->position.y = i;\ + if (doDepth) /*constant condition - compiler should optimize it out*/\ + pixelInfo->depth += depthIncrement;\ + if (doCoords) /*constant condition - compiler should optimize it out*/\ + {\ + RCL_Unit d = _RCL_floorPixelDistances[i];\ + RCL_Unit d2 = RCL_nonZero(pixelInfo->hit.distance);\ + pixelInfo->texCoords.x =\ + _RCL_camera.position.x + ((d * dx) / d2);\ + pixelInfo->texCoords.y =\ + _RCL_camera.position.y + ((d * dy) / d2);\ + }\ + RCL_PIXEL_FUNCTION(pixelInfo);\ + }\ + } + + if (computeDepth) // branch early + { + if (!computeCoords) + loop(1,0) + else + loop(1,1) + } + else + { + if (!computeCoords) + loop(0,0) + else + loop(1,1) + } + + #undef loop + + return limit; +} + +/// Helper for drawing walls. Returns the last drawn pixel position. +static inline int16_t _RCL_drawWall( + RCL_Unit yCurrent, + RCL_Unit yFrom, + RCL_Unit yTo, + RCL_Unit limit1, // TODO: int16_t? + RCL_Unit limit2, + RCL_Unit height, + int16_t increment, + RCL_PixelInfo *pixelInfo + ) +{ + pixelInfo->isWall = 1; + + RCL_Unit limit = RCL_clamp(yTo,limit1,limit2); + + RCL_Unit wallLength = yTo - yFrom - 1; + wallLength = RCL_nonZero(wallLength); + + RCL_Unit wallPosition = RCL_absVal(yFrom - yCurrent) - increment; + + RCL_Unit coordStep = RCL_COMPUTE_WALL_TEXCOORDS ? +#if RCL_TEXTURE_VERTICAL_STRETCH == 1 + RCL_UNITS_PER_SQUARE / wallLength +#else + height / wallLength +#endif + : 1; + + pixelInfo->texCoords.y = RCL_COMPUTE_WALL_TEXCOORDS ? + wallPosition * coordStep : 0; + +#if RCL_ACCURATE_WALL_TEXTURING == 1 + if (1) +#else + if (RCL_absVal(coordStep) < RCL_MIN_TEXTURE_STEP) + /* for the sake of performance there are two-versions of the loop - it's + better to branch early than inside the loop */ +#endif + { + for (RCL_Unit i = yCurrent + increment; + increment == -1 ? i >= limit : i <= limit; // TODO: is efficient? + i += increment) + { + // more expensive texture coord computing + pixelInfo->position.y = i; + +#if RCL_COMPUTE_WALL_TEXCOORDS == 1 + #if RCL_TEXTURE_VERTICAL_STRETCH == 1 + pixelInfo->texCoords.y = (wallPosition * RCL_UNITS_PER_SQUARE) / wallLength; + #else + pixelInfo->texCoords.y = (wallPosition * height) / wallLength; + #endif +#endif + + wallPosition++; + RCL_PIXEL_FUNCTION(pixelInfo); + } + } + else + { + for (RCL_Unit i = yCurrent + increment; + increment == -1 ? i >= limit : i <= limit; // TODO: is efficient? + i += increment) + { + // cheaper texture coord computing + + pixelInfo->position.y = i; + +#if RCL_COMPUTE_WALL_TEXCOORDS == 1 + pixelInfo->texCoords.y += coordStep; +#endif + + RCL_PIXEL_FUNCTION(pixelInfo); + } + } + + return limit; +} + +/// Fills a RCL_HitResult struct with info for a hit at infinity. +static inline void _RCL_makeInfiniteHit(RCL_HitResult *hit, RCL_Ray *ray) +{ + hit->distance = RCL_UNITS_PER_SQUARE * RCL_UNITS_PER_SQUARE; + /* ^ horizon is at infinity, but we can't use too big infinity + (RCL_INFINITY) because it would overflow in the following mult. */ + hit->position.x = (ray->direction.x * hit->distance) / RCL_UNITS_PER_SQUARE; + hit->position.y = (ray->direction.y * hit->distance) / RCL_UNITS_PER_SQUARE; + + hit->direction = 0; + hit->textureCoord = 0; + hit->arrayValue = 0; + hit->doorRoll = 0; + hit->type = 0; +} + +void _RCL_columnFunctionComplex(RCL_HitResult *hits, uint16_t hitCount, uint16_t x, + RCL_Ray ray) +{ + // last written Y position, can never go backwards + RCL_Unit fPosY = _RCL_camera.resolution.y; + RCL_Unit cPosY = -1; + + // world coordinates (relative to camera height though) + RCL_Unit fZ1World = _RCL_startFloorHeight; + RCL_Unit cZ1World = _RCL_startCeil_Height; + + RCL_PixelInfo p; + p.position.x = x; + p.height = 0; + p.texCoords.x = 0; + p.texCoords.y = 0; + + // we'll be simulatenously drawing the floor and the ceiling now + for (RCL_Unit j = 0; j <= hitCount; ++j) + { // ^ = add extra iteration for horizon plane + int8_t drawingHorizon = j == hitCount; + + RCL_HitResult hit; + RCL_Unit distance = 1; + + RCL_Unit fWallHeight = 0, cWallHeight = 0; + RCL_Unit fZ2World = 0, cZ2World = 0; + RCL_Unit fZ1Screen = 0, cZ1Screen = 0; + RCL_Unit fZ2Screen = 0, cZ2Screen = 0; + + if (!drawingHorizon) + { + hit = hits[j]; + distance = RCL_nonZero(hit.distance); + p.hit = hit; + + fWallHeight = _RCL_floorFunction(hit.square.x,hit.square.y); + fZ2World = fWallHeight - _RCL_camera.height; + fZ1Screen = _RCL_middleRow - RCL_perspectiveScale( + (fZ1World * _RCL_camera.resolution.y) / + RCL_UNITS_PER_SQUARE,distance); + fZ2Screen = _RCL_middleRow - RCL_perspectiveScale( + (fZ2World * _RCL_camera.resolution.y) / + RCL_UNITS_PER_SQUARE,distance); + + if (_RCL_ceilFunction != 0) + { + cWallHeight = _RCL_ceilFunction(hit.square.x,hit.square.y); + cZ2World = cWallHeight - _RCL_camera.height; + cZ1Screen = _RCL_middleRow - RCL_perspectiveScale( + (cZ1World * _RCL_camera.resolution.y) / + RCL_UNITS_PER_SQUARE,distance); + cZ2Screen = _RCL_middleRow - RCL_perspectiveScale( + (cZ2World * _RCL_camera.resolution.y) / + RCL_UNITS_PER_SQUARE,distance); + } + } + else + { + fZ1Screen = _RCL_middleRow; + cZ1Screen = _RCL_middleRow + 1; + _RCL_makeInfiniteHit(&p.hit,&ray); + } + + RCL_Unit limit; + + p.isWall = 0; + p.isHorizon = drawingHorizon; + + // draw floor until wall + p.isFloor = 1; + p.height = fZ1World + _RCL_camera.height; + +#if RCL_COMPUTE_FLOOR_DEPTH == 1 + p.depth = (_RCL_fHorizontalDepthStart - fPosY) * _RCL_horizontalDepthStep; +#else + p.depth = 0; +#endif + + limit = _RCL_drawHorizontal(fPosY,fZ1Screen,cPosY + 1, + _RCL_camera.resolution.y,fZ1World,-1,RCL_COMPUTE_FLOOR_DEPTH, + // ^ purposfully allow outside screen bounds + RCL_COMPUTE_FLOOR_TEXCOORDS && p.height == RCL_FLOOR_TEXCOORDS_HEIGHT, + 1,&ray,&p); + + if (fPosY > limit) + fPosY = limit; + + if (_RCL_ceilFunction != 0 || drawingHorizon) + { + // draw ceiling until wall + p.isFloor = 0; + p.height = cZ1World + _RCL_camera.height; + +#if RCL_COMPUTE_CEILING_DEPTH == 1 + p.depth = (cPosY - _RCL_cHorizontalDepthStart) * + _RCL_horizontalDepthStep; +#endif + + limit = _RCL_drawHorizontal(cPosY,cZ1Screen, + -1,fPosY - 1,cZ1World,1,RCL_COMPUTE_CEILING_DEPTH,0,1,&ray,&p); + // ^ purposfully allow outside screen bounds here + + if (cPosY < limit) + cPosY = limit; + } + + if (!drawingHorizon) // don't draw walls for horizon plane + { + p.isWall = 1; + p.depth = distance; + p.isFloor = 1; + p.texCoords.x = hit.textureCoord; + p.height = 0; // don't compute this, no use + + // draw floor wall + + if (fPosY > 0) // still pixels left? + { + p.isFloor = 1; + + limit = _RCL_drawWall(fPosY,fZ1Screen,fZ2Screen,cPosY + 1, + _RCL_camera.resolution.y, + // ^ purposfully allow outside screen bounds here +#if RCL_TEXTURE_VERTICAL_STRETCH == 1 + RCL_UNITS_PER_SQUARE +#else + fZ2World - fZ1World +#endif + ,-1,&p); + + + if (fPosY > limit) + fPosY = limit; + + fZ1World = fZ2World; // for the next iteration + } // ^ purposfully allow outside screen bounds here + + // draw ceiling wall + + if (_RCL_ceilFunction != 0 && cPosY < _RCL_camResYLimit) // pixels left? + { + p.isFloor = 0; + + limit = _RCL_drawWall(cPosY,cZ1Screen,cZ2Screen, + -1,fPosY - 1, + // ^ puposfully allow outside screen bounds here +#if RCL_TEXTURE_VERTICAL_STRETCH == 1 + RCL_UNITS_PER_SQUARE +#else + cZ2World - cZ1World +#endif + ,1,&p); + + if (cPosY < limit) + cPosY = limit; + + cZ1World = cZ2World; // for the next iteration + } // ^ puposfully allow outside screen bounds here + } + } +} + +void _RCL_columnFunctionSimple(RCL_HitResult *hits, uint16_t hitCount, + uint16_t x, RCL_Ray ray) +{ + RCL_Unit y = 0; + RCL_Unit wallHeightScreen = 0; + RCL_Unit wallStart = _RCL_middleRow; + RCL_Unit heightOffset = 0; + + RCL_Unit dist = 1; + + RCL_PixelInfo p; + p.position.x = x; + + if (hitCount > 0) + { + RCL_HitResult hit = hits[0]; + + uint8_t goOn = 1; + + if (_RCL_rollFunction != 0 && RCL_COMPUTE_WALL_TEXCOORDS == 1) + { + if (hit.arrayValue == 0) + { + // standing inside door square, looking out => move to the next hit + + if (hitCount > 1) + hit = hits[1]; + else + goOn = 0; + } + else + { + // normal hit, check the door roll + + RCL_Unit texCoordMod = hit.textureCoord % RCL_UNITS_PER_SQUARE; + + int8_t unrolled = hit.doorRoll >= 0 ? + (hit.doorRoll > texCoordMod) : + (texCoordMod > RCL_UNITS_PER_SQUARE + hit.doorRoll); + + if (unrolled) + { + goOn = 0; + + if (hitCount > 1) /* should probably always be true (hit on square + exit) */ + { + if (hit.direction % 2 != hits[1].direction % 2) + { + // hit on the inner side + hit = hits[1]; + goOn = 1; + } + else if (hitCount > 2) + { + // hit on the opposite side + hit = hits[2]; + goOn = 1; + } + } + } + } + } + + p.hit = hit; + + if (goOn) + { + dist = hit.distance; + + int16_t wallHeightWorld = _RCL_floorFunction(hit.square.x,hit.square.y); + + wallHeightScreen = RCL_perspectiveScale((wallHeightWorld * + _RCL_camera.resolution.y) / RCL_UNITS_PER_SQUARE,dist); + + int16_t RCL_normalizedWallHeight = wallHeightWorld != 0 ? + ((RCL_UNITS_PER_SQUARE * wallHeightScreen) / wallHeightWorld) : 0; + + heightOffset = RCL_perspectiveScale(_RCL_cameraHeightScreen,dist); + + wallStart = _RCL_middleRow - wallHeightScreen + heightOffset + + RCL_normalizedWallHeight; + } + } + else + { + _RCL_makeInfiniteHit(&p.hit,&ray); + } + + // draw ceiling + + p.isWall = 0; + p.isFloor = 0; + p.isHorizon = 1; + p.depth = 1; + p.height = RCL_UNITS_PER_SQUARE; + + y = _RCL_drawHorizontal(-1,wallStart,-1,_RCL_middleRow,_RCL_camera.height,1, + RCL_COMPUTE_CEILING_DEPTH,0,1,&ray,&p); + + // draw wall + + p.isWall = 1; + p.isFloor = 1; + p.depth = dist; + p.height = 0; + +#if RCL_ROLL_TEXTURE_COORDS == 1 && RCL_COMPUTE_WALL_TEXCOORDS == 1 + p.hit.textureCoord -= p.hit.doorRoll; +#endif + + p.texCoords.x = p.hit.textureCoord; + p.texCoords.y = 0; + + RCL_Unit limit = _RCL_drawWall(y,wallStart,wallStart + wallHeightScreen - 1, + -1,_RCL_camResYLimit,p.hit.arrayValue,1,&p); + + y = RCL_max(y,limit); // take max, in case no wall was drawn + y = RCL_max(y,wallStart); + + // draw floor + + p.isWall = 0; + +#if RCL_COMPUTE_FLOOR_DEPTH == 1 + p.depth = (_RCL_camera.resolution.y - y) * _RCL_horizontalDepthStep + 1; +#endif + + _RCL_drawHorizontal(y,_RCL_camResYLimit,-1,_RCL_camResYLimit, + _RCL_camera.height,1,RCL_COMPUTE_FLOOR_DEPTH,RCL_COMPUTE_FLOOR_TEXCOORDS, + -1,&ray,&p); +} + +/** + Precomputes a distance from camera to the floor at each screen row into an + array (must be preallocated with sufficient (camera.resolution.y) length). +*/ +static inline void _RCL_precomputeFloorDistances(RCL_Camera camera, + RCL_Unit *dest, uint16_t startIndex) +{ + RCL_Unit camHeightScreenSize = + (camera.height * camera.resolution.y) / RCL_UNITS_PER_SQUARE; + + for (uint16_t i = startIndex +; i < camera.resolution.y; ++i) + dest[i] = RCL_perspectiveScaleInverse(camHeightScreenSize, + RCL_absVal(i - _RCL_middleRow)); +} + +void RCL_renderComplex(RCL_Camera cam, RCL_ArrayFunction floorHeightFunc, + RCL_ArrayFunction ceilingHeightFunc, RCL_ArrayFunction typeFunction, + RCL_RayConstraints constraints) +{ + _RCL_floorFunction = floorHeightFunc; + _RCL_ceilFunction = ceilingHeightFunc; + _RCL_camera = cam; + _RCL_camResYLimit = cam.resolution.y - 1; + + uint16_t halfResY = cam.resolution.y / 2; + + _RCL_middleRow = halfResY + cam.shear; + + _RCL_fHorizontalDepthStart = _RCL_middleRow + halfResY; + _RCL_cHorizontalDepthStart = _RCL_middleRow - halfResY; + + _RCL_startFloorHeight = floorHeightFunc( + RCL_divRoundDown(cam.position.x,RCL_UNITS_PER_SQUARE), + RCL_divRoundDown(cam.position.y,RCL_UNITS_PER_SQUARE)) -1 * cam.height; + + _RCL_startCeil_Height = + ceilingHeightFunc != 0 ? + ceilingHeightFunc( + RCL_divRoundDown(cam.position.x,RCL_UNITS_PER_SQUARE), + RCL_divRoundDown(cam.position.y,RCL_UNITS_PER_SQUARE)) -1 * cam.height + : RCL_INFINITY; + + _RCL_horizontalDepthStep = RCL_HORIZON_DEPTH / cam.resolution.y; + +#if RCL_COMPUTE_FLOOR_TEXCOORDS == 1 + RCL_Unit floorPixelDistances[cam.resolution.y]; + _RCL_precomputeFloorDistances(cam,floorPixelDistances,0); + _RCL_floorPixelDistances = floorPixelDistances; // pass to column function +#endif + + RCL_castRaysMultiHit(cam,_RCL_floorCeilFunction,typeFunction, + _RCL_columnFunctionComplex,constraints); +} + +void RCL_renderSimple(RCL_Camera cam, RCL_ArrayFunction floorHeightFunc, + RCL_ArrayFunction typeFunc, RCL_ArrayFunction rollFunc, + RCL_RayConstraints constraints) +{ + _RCL_floorFunction = floorHeightFunc; + _RCL_camera = cam; + _RCL_camResYLimit = cam.resolution.y - 1; + _RCL_middleRow = cam.resolution.y / 2; + _RCL_rollFunction = rollFunc; + + _RCL_cameraHeightScreen = + (_RCL_camera.resolution.y * (_RCL_camera.height - RCL_UNITS_PER_SQUARE)) / + RCL_UNITS_PER_SQUARE; + + _RCL_horizontalDepthStep = RCL_HORIZON_DEPTH / cam.resolution.y; + + constraints.maxHits = + _RCL_rollFunction == 0 ? + 1 : // no door => 1 hit is enough + 3; // for correctly rendering rolling doors we'll need 3 hits (NOT 2) + +#if RCL_COMPUTE_FLOOR_TEXCOORDS == 1 + RCL_Unit floorPixelDistances[cam.resolution.y]; + _RCL_precomputeFloorDistances(cam,floorPixelDistances,_RCL_middleRow); + _RCL_floorPixelDistances = floorPixelDistances; // pass to column function +#endif + + RCL_castRaysMultiHit(cam,_floorHeightNotZeroFunction,typeFunc, + _RCL_columnFunctionSimple, constraints); + +#if RCL_COMPUTE_FLOOR_TEXCOORDS == 1 + _RCL_floorPixelDistances = 0; +#endif +} + +RCL_Vector2D RCL_normalize(RCL_Vector2D v) +{ + RCL_profileCall(RCL_normalize); + + RCL_Vector2D result; + RCL_Unit l = RCL_len(v); + l = RCL_nonZero(l); + + result.x = (v.x * RCL_UNITS_PER_SQUARE) / l; + result.y = (v.y * RCL_UNITS_PER_SQUARE) / l; + + return result; +} + +RCL_Unit RCL_vectorsAngleCos(RCL_Vector2D v1, RCL_Vector2D v2) +{ + RCL_profileCall(RCL_vectorsAngleCos); + + v1 = RCL_normalize(v1); + v2 = RCL_normalize(v2); + + return (v1.x * v2.x + v1.y * v2.y) / RCL_UNITS_PER_SQUARE; +} + +RCL_PixelInfo RCL_mapToScreen(RCL_Vector2D worldPosition, RCL_Unit height, + RCL_Camera camera) +{ + RCL_PixelInfo result; + + RCL_Unit d = RCL_dist(worldPosition,camera.position); + + RCL_Vector2D toPoint; + + toPoint.x = worldPosition.x - camera.position.x; + toPoint.y = worldPosition.y - camera.position.y; + + RCL_Vector2D cameraDir = RCL_angleToDirection(camera.direction); + + result.depth = // adjusted distance + (d * RCL_vectorsAngleCos(cameraDir,toPoint)) / RCL_UNITS_PER_SQUARE; + + result.position.y = camera.resolution.y / 2 - + (camera.resolution.y * + RCL_perspectiveScale(height - camera.height,result.depth)) / RCL_UNITS_PER_SQUARE + + camera.shear; + + RCL_Unit middleColumn = camera.resolution.x / 2; + + RCL_Unit a = RCL_sqrtInt(d * d - result.depth * result.depth); + + RCL_Ray r; + r.start = camera.position; + r.direction = cameraDir; + + if (!RCL_pointIfLeftOfRay(worldPosition,r)) + a *= -1; + + RCL_Unit cos = RCL_cosInt(RCL_HORIZONTAL_FOV_HALF); + + RCL_Unit b = (result.depth * RCL_sinInt(RCL_HORIZONTAL_FOV_HALF)) / + RCL_nonZero(cos); + // sin/cos = tan + + result.position.x = (a * middleColumn) / RCL_nonZero(b); + result.position.x = middleColumn - result.position.x; + + return result; +} + +RCL_Unit RCL_degreesToUnitsAngle(int16_t degrees) +{ + return (degrees * RCL_UNITS_PER_SQUARE) / 360; +} + +RCL_Unit RCL_perspectiveScale(RCL_Unit originalSize, RCL_Unit distance) +{ + RCL_profileCall(RCL_perspectiveScale); + + return distance != 0 ? + (originalSize * RCL_UNITS_PER_SQUARE) / + ((RCL_VERTICAL_FOV * 2 * distance) / RCL_UNITS_PER_SQUARE) + : 0; +} + +RCL_Unit RCL_perspectiveScaleInverse(RCL_Unit originalSize, + RCL_Unit scaledSize) +{ + return scaledSize != 0 ? + (originalSize * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2) / + // ^ take the middle + ((RCL_VERTICAL_FOV * 2 * scaledSize) / RCL_UNITS_PER_SQUARE) + : RCL_INFINITY; +} + +void RCL_moveCameraWithCollision(RCL_Camera *camera, RCL_Vector2D planeOffset, + RCL_Unit heightOffset, RCL_ArrayFunction floorHeightFunc, + RCL_ArrayFunction ceilingHeightFunc, int8_t computeHeight, int8_t force) +{ + int8_t movesInPlane = planeOffset.x != 0 || planeOffset.y != 0; + int16_t xSquareNew, ySquareNew; + + if (movesInPlane || force) + { + RCL_Vector2D corner; // BBox corner in the movement direction + RCL_Vector2D cornerNew; + + int16_t xDir = planeOffset.x > 0 ? 1 : -1; + int16_t yDir = planeOffset.y > 0 ? 1 : -1; + + corner.x = camera->position.x + xDir * RCL_CAMERA_COLL_RADIUS; + corner.y = camera->position.y + yDir * RCL_CAMERA_COLL_RADIUS; + + int16_t xSquare = RCL_divRoundDown(corner.x,RCL_UNITS_PER_SQUARE); + int16_t ySquare = RCL_divRoundDown(corner.y,RCL_UNITS_PER_SQUARE); + + cornerNew.x = corner.x + planeOffset.x; + cornerNew.y = corner.y + planeOffset.y; + + xSquareNew = RCL_divRoundDown(cornerNew.x,RCL_UNITS_PER_SQUARE); + ySquareNew = RCL_divRoundDown(cornerNew.y,RCL_UNITS_PER_SQUARE); + + RCL_Unit bottomLimit = -1 * RCL_INFINITY; + RCL_Unit topLimit = RCL_INFINITY; + + if (computeHeight) + { + bottomLimit = camera->height - RCL_CAMERA_COLL_HEIGHT_BELOW + + RCL_CAMERA_COLL_STEP_HEIGHT; + + topLimit = camera->height + RCL_CAMERA_COLL_HEIGHT_ABOVE; + } + + // checks a single square for collision against the camera + #define collCheck(dir,s1,s2)\ + if (computeHeight)\ + {\ + RCL_Unit height = floorHeightFunc(s1,s2);\ + if (height > bottomLimit)\ + dir##Collides = 1;\ + else if (ceilingHeightFunc != 0)\ + {\ + height = ceilingHeightFunc(s1,s2);\ + if (height < topLimit)\ + dir##Collides = 1;\ + }\ + }\ + else\ + dir##Collides = floorHeightFunc(s1,s2) > RCL_CAMERA_COLL_STEP_HEIGHT; + + // check a collision against non-diagonal square + #define collCheckOrtho(dir,dir2,s1,s2,x)\ + if (dir##SquareNew != dir##Square)\ + {\ + collCheck(dir,s1,s2)\ + }\ + if (!dir##Collides)\ + { /* now also check for coll on the neighbouring square */ \ + int16_t dir2##Square2 = RCL_divRoundDown(corner.dir2 - dir2##Dir *\ + RCL_CAMERA_COLL_RADIUS * 2,RCL_UNITS_PER_SQUARE);\ + if (dir2##Square2 != dir2##Square)\ + {\ + if (x)\ + collCheck(dir,dir##SquareNew,dir2##Square2)\ + else\ + collCheck(dir,dir2##Square2,dir##SquareNew)\ + }\ + } + + int8_t xCollides = 0; + collCheckOrtho(x,y,xSquareNew,ySquare,1) + + int8_t yCollides = 0; + collCheckOrtho(y,x,xSquare,ySquareNew,0) + + #define collHandle(dir)\ + if (dir##Collides)\ + cornerNew.dir = (dir##Square) * RCL_UNITS_PER_SQUARE +\ + RCL_UNITS_PER_SQUARE / 2 + dir##Dir * (RCL_UNITS_PER_SQUARE / 2) -\ + dir##Dir;\ + + if (!xCollides && !yCollides) /* if non-diagonal collision happend, corner + collision can't happen */ + { + if (xSquare != xSquareNew && ySquare != ySquareNew) // corner? + { + int8_t xyCollides = 0; + collCheck(xy,xSquareNew,ySquareNew) + + if (xyCollides) + { + // normally should slide, but let's KISS + cornerNew = corner; + } + } + } + + collHandle(x) + collHandle(y) + + #undef collCheck + #undef collHandle + + camera->position.x = cornerNew.x - xDir * RCL_CAMERA_COLL_RADIUS; + camera->position.y = cornerNew.y - yDir * RCL_CAMERA_COLL_RADIUS; + } + + if (computeHeight && (movesInPlane || heightOffset != 0 || force)) + { + camera->height += heightOffset; + + int16_t xSquare1 = RCL_divRoundDown(camera->position.x - + RCL_CAMERA_COLL_RADIUS,RCL_UNITS_PER_SQUARE); + + int16_t xSquare2 = RCL_divRoundDown(camera->position.x + + RCL_CAMERA_COLL_RADIUS,RCL_UNITS_PER_SQUARE); + + int16_t ySquare1 = RCL_divRoundDown(camera->position.y - + RCL_CAMERA_COLL_RADIUS,RCL_UNITS_PER_SQUARE); + + int16_t ySquare2 = RCL_divRoundDown(camera->position.y + + RCL_CAMERA_COLL_RADIUS,RCL_UNITS_PER_SQUARE); + + RCL_Unit bottomLimit = floorHeightFunc(xSquare1,ySquare1); + RCL_Unit topLimit = ceilingHeightFunc != 0 ? + ceilingHeightFunc(xSquare1,ySquare1) : RCL_INFINITY; + + RCL_Unit height; + + #define checkSquares(s1,s2)\ + {\ + height = floorHeightFunc(xSquare##s1,ySquare##s2);\ + bottomLimit = RCL_max(bottomLimit,height);\ + height = ceilingHeightFunc != 0 ?\ + ceilingHeightFunc(xSquare##s1,ySquare##s2) : RCL_INFINITY;\ + topLimit = RCL_min(topLimit,height);\ + } + + if (xSquare2 != xSquare1) + checkSquares(2,1) + + if (ySquare2 != ySquare1) + checkSquares(1,2) + + if (xSquare2 != xSquare1 && ySquare2 != ySquare1) + checkSquares(2,2) + + camera->height = RCL_clamp(camera->height, + bottomLimit + RCL_CAMERA_COLL_HEIGHT_BELOW, + topLimit - RCL_CAMERA_COLL_HEIGHT_ABOVE); + + #undef checkSquares + } +} + +void RCL_initCamera(RCL_Camera *camera) +{ + camera->position.x = 0; + camera->position.y = 0; + camera->direction = 0; + camera->resolution.x = 20; + camera->resolution.y = 15; + camera->shear = 0; + camera->height = RCL_UNITS_PER_SQUARE; +} + +void RCL_initRayConstraints(RCL_RayConstraints *constraints) +{ + constraints->maxHits = 1; + constraints->maxSteps = 20; +} + +#endif