1
0
Fork 0
mirror of https://git.coom.tech/drummyfish/raycastlib.git synced 2024-11-21 20:29:59 +01:00
raycastlib/raycastlib.h

945 lines
24 KiB
C
Raw Normal View History

2018-08-23 02:46:40 +02:00
#ifndef RAYCASTLIB_H
#define RAYCASTLIB_H
2018-08-21 13:49:45 +02:00
/**
2018-09-01 07:27:17 +02:00
raycastlib - Small C header-only raycasting library for embedded and low
performance computers, such as Arduino. Only uses integer math and stdint
standard library.
2018-08-21 13:49:45 +02:00
author: Miloslav "drummyfish" Ciz
license: CC0
- Game field's bottom left corner is at [0,0].
- X axis goes right.
- Y axis goes up.
- Each game square is UNITS_PER_SQUARE * UNITS_PER_SQUARE.
2018-08-23 09:35:10 +02:00
- Angles are in Units, 0 means pointing right (x+) and positively rotates
clockwise, a full angle has UNITS_PER_SQUARE Units.
2018-08-21 13:49:45 +02:00
*/
2018-09-01 07:27:17 +02:00
#include <stdint.h>
2018-09-02 13:58:46 +02:00
#ifndef RAYCAST_TINY
2018-09-05 12:20:46 +02:00
#define UNITS_PER_SQUARE 1024 ///< N. of Units in a side of a spatial square.
typedef int32_t Unit; /**< Smallest spatial unit, there is UNITS_PER_SQUARE
2018-09-01 07:27:17 +02:00
units in a square's length. This effectively
serves the purpose of a fixed-point arithmetic. */
2018-09-05 12:20:46 +02:00
#define UNIT_INFINITY 2147483647;
2018-09-02 13:58:46 +02:00
2018-09-05 12:20:46 +02:00
typedef int32_t int_maybe32_t;
typedef uint32_t uint_maybe32_t;
2018-09-02 13:58:46 +02:00
#else
2018-09-05 12:20:46 +02:00
#define UNITS_PER_SQUARE 64
typedef int16_t Unit;
#define UNIT_INFINITY 32767;
typedef int16_t int_maybe32_t;
typedef uint16_t uint_maybe32_t;
2018-09-02 13:58:46 +02:00
#endif
2018-08-21 13:49:45 +02:00
2018-09-05 16:26:13 +02:00
#ifndef VERTICAL_FOV
2018-09-03 16:38:49 +02:00
#define VERTICAL_FOV (UNITS_PER_SQUARE / 2)
2018-09-05 16:26:13 +02:00
#endif
2018-09-03 16:38:49 +02:00
2018-08-23 03:04:52 +02:00
#define logVector2D(v)\
printf("[%d,%d]\n",v.x,v.y);
2018-08-23 09:35:10 +02:00
#define logRay(r){\
2018-08-23 03:04:52 +02:00
printf("ray:\n");\
printf(" start: ");\
logVector2D(r.start);\
printf(" dir: ");\
2018-08-23 09:35:10 +02:00
logVector2D(r.direction);}\
2018-08-23 03:04:52 +02:00
2018-08-23 09:35:10 +02:00
#define logHitResult(h){\
2018-08-23 03:04:52 +02:00
printf("hit:\n");\
printf(" sqaure: ");\
logVector2D(h.square);\
printf(" pos: ");\
logVector2D(h.position);\
2018-08-30 08:51:53 +02:00
printf(" dist: %d\n", h.distance);\
printf(" texcoord: %d\n", h.textureCoord);}\
2018-08-23 03:04:52 +02:00
2018-09-04 13:01:14 +02:00
#define logPixelInfo(p){\
printf("pixel:\n");\
printf(" position: ");\
logVector2D(p.position);\
printf(" depth: %d\n", p.depth);\
printf(" wall: %d\n", p.isWall);\
printf(" textCoordY: %d\n", p.textureCoordY);\
printf(" hit: ");\
logHitResult(p.hit);\
}\
2018-08-21 13:49:45 +02:00
/// Position in 2D space.
typedef struct
{
2018-09-04 18:55:18 +02:00
Unit y;
Unit x;
2018-08-21 13:49:45 +02:00
} Vector2D;
typedef struct
{
Vector2D start;
Vector2D direction;
} Ray;
2018-08-22 08:21:38 +02:00
typedef struct
{
Vector2D square; ///< Collided square coordinates.
Vector2D position; ///< Exact collision position in Units.
Unit distance; /**< Euclidean distance to the hit position, or -1 if
no collision happened. */
2018-09-05 16:26:13 +02:00
Unit textureCoord; ///< Normalized (0 to UNITS_PER_SQUARE - 1) tex coord.
Unit type; ///< Integer identifying type of square.
2018-09-03 14:23:11 +02:00
uint8_t direction; ///< Direction of hit.
2018-08-22 08:21:38 +02:00
} HitResult;
2018-09-03 16:38:49 +02:00
// TODO: things like FOV could be constants to make them precomp. and faster?
2018-08-31 11:51:03 +02:00
typedef struct
{
2018-08-31 13:29:50 +02:00
Vector2D position;
Unit direction;
Vector2D resolution;
Unit fovAngle;
Unit height;
} Camera;
typedef struct
{
2018-09-03 14:23:11 +02:00
Vector2D position; ///< On-screen position.
2018-09-05 19:08:38 +02:00
int8_t isWall; ///< Whether the pixel is a wall or a floor/ceiling.
int8_t isFloor; ///< Whether the pixel is floor or ceiling.
2018-09-03 14:23:11 +02:00
Unit depth; ///< Corrected depth.
HitResult hit; ///< Corresponding ray hit.
Unit textureCoordY; ///< Normalized (0 to UNITS_PER_SQUARE - 1) tex coord.
2018-08-31 11:51:03 +02:00
} PixelInfo;
2018-08-31 13:29:50 +02:00
typedef struct
{
uint16_t maxHits;
uint16_t maxSteps;
2018-09-05 11:44:15 +02:00
uint8_t computeTextureCoords; ///< Turns texture coords on/off.
2018-08-31 13:29:50 +02:00
} RayConstraints;
2018-09-01 07:27:17 +02:00
/**
Function used to retrieve the cells of the rendered scene. It should return
a "type" of given square as an integer (e.g. square height) - between squares
that return different numbers there is considered to be a collision.
*/
2018-09-01 09:07:49 +02:00
typedef Unit (*ArrayFunction)(int16_t x, int16_t y);
2018-09-01 07:27:17 +02:00
2018-08-31 18:11:32 +02:00
typedef void (*PixelFunction)(PixelInfo info);
2018-08-31 11:51:03 +02:00
2018-09-01 07:27:17 +02:00
typedef void
(*ColumnFunction)(HitResult *hits, uint16_t hitCount, uint16_t x, Ray ray);
2018-08-23 00:50:19 +02:00
/**
2018-09-01 07:27:17 +02:00
Simple-interface function to cast a single ray.
2018-08-23 00:50:19 +02:00
@return The first collision result.
*/
2018-08-31 13:29:50 +02:00
HitResult castRay(Ray ray, ArrayFunction arrayFunc);
2018-08-21 13:49:45 +02:00
2018-09-04 11:42:27 +02:00
/**
Maps a single point in the world to the screen (2D position + depth).
*/
PixelInfo mapToScreen(Vector2D worldPosition, Unit height, Camera camera);
2018-08-23 02:36:51 +02:00
/**
Casts a single ray and returns a list of collisions.
*/
2018-09-05 16:26:13 +02:00
void castRayMultiHit(Ray ray, ArrayFunction arrayFunc, ArrayFunction typeFunc,
HitResult *hitResults, uint16_t *hitResultsLen, RayConstraints constraints);
2018-08-23 02:36:51 +02:00
2018-08-23 09:35:10 +02:00
Vector2D angleToDirection(Unit angle);
2018-08-23 09:25:34 +02:00
Unit cosInt(Unit input);
Unit sinInt(Unit input);
2018-08-30 18:31:08 +02:00
/// Normalizes given vector to have UNITS_PER_SQUARE length.
Vector2D normalize(Vector2D v);
/// Computes a cos of an angle between two vectors.
Unit vectorsAngleCos(Vector2D v1, Vector2D v2);
2018-09-02 13:58:46 +02:00
uint16_t sqrtInt(uint_maybe32_t value);
2018-08-23 09:25:34 +02:00
Unit dist(Vector2D p1, Vector2D p2);
2018-08-30 14:44:14 +02:00
Unit len(Vector2D v);
2018-08-23 09:25:34 +02:00
2018-08-30 08:51:53 +02:00
/**
Converts an angle in whole degrees to an angle in Units that this library
uses.
*/
Unit degreesToUnitsAngle(int16_t degrees);
///< Computes the change in size of an object due to perspective.
2018-09-03 16:38:49 +02:00
Unit perspectiveScale(Unit originalSize, Unit distance);
2018-08-30 08:51:53 +02:00
/**
Casts rays for given camera view and for each hit calls a user provided
function.
*/
2018-08-31 18:11:32 +02:00
void castRaysMultiHit(Camera cam, ArrayFunction arrayFunc,
2018-09-05 16:26:13 +02:00
ArrayFunction typeFunction, ColumnFunction columnFunc,
RayConstraints constraints);
2018-08-30 08:51:53 +02:00
2018-09-05 12:20:46 +02:00
/**
Renders a complete camera view.
@param cam camera whose view to render
@param floorHeightFunc function that returns floor height (in Units)
@param ceilingHeightFunc same as floorHeightFunc but for ceiling, can also be
0 (no ceiling will be rendered)
2018-09-05 16:26:13 +02:00
@param typeFunction function that says a type of square (e.g. its texture
index), can be 0 (no type in hit result)
2018-09-05 12:20:46 +02:00
@param pixelFunc callback function to draw a single pixel on screen
@param constraints constraints for each cast ray
*/
2018-09-05 16:26:13 +02:00
void render(Camera cam, ArrayFunction floorHeightFunc,
ArrayFunction ceilingHeightFunc, ArrayFunction typeFunction,
PixelFunction pixelFunc, RayConstraints constraints);
2018-08-31 18:26:51 +02:00
2018-08-21 13:49:45 +02:00
//=============================================================================
// privates
2018-08-31 15:38:02 +02:00
#ifdef RAYCASTLIB_PROFILE
// function call counters for profiling
2018-09-04 18:55:18 +02:00
uint32_t profile_sqrtInt = 0;
uint32_t profile_clamp = 0;
uint32_t profile_cosInt = 0;
uint32_t profile_angleToDirection = 0;
uint32_t profile_dist = 0;
uint32_t profile_len = 0;
uint32_t profile_pointIsLeftOfRay = 0;
uint32_t profile_castRaySquare = 0;
uint32_t profile_castRayMultiHit = 0;
uint32_t profile_castRay = 0;
uint32_t profile_absVal = 0;
uint32_t profile_normalize = 0;
uint32_t profile_vectorsAngleCos = 0;
uint32_t profile_perspectiveScale = 0;
2018-09-04 19:32:47 +02:00
uint32_t profile_wrap = 0;
uint32_t profile_divRoundDown = 0;
2018-08-31 15:38:02 +02:00
#define profileCall(c) profile_##c += 1
#define printProfile() {\
printf("profile:\n");\
2018-08-31 16:46:55 +02:00
printf(" sqrtInt: %d\n",profile_sqrtInt);\
printf(" clamp: %d\n",profile_clamp);\
printf(" cosInt: %d\n",profile_cosInt);\
printf(" angleToDirection: %d\n",profile_angleToDirection);\
printf(" dist: %d\n",profile_dist);\
printf(" len: %d\n",profile_len);\
printf(" pointIsLeftOfRay: %d\n",profile_pointIsLeftOfRay);\
printf(" castRaySquare: %d\n",profile_castRaySquare);\
printf(" castRayMultiHit : %d\n",profile_castRayMultiHit);\
printf(" castRay: %d\n",profile_castRay);\
printf(" normalize: %d\n",profile_normalize);\
2018-09-04 18:55:18 +02:00
printf(" vectorsAngleCos: %d\n",profile_vectorsAngleCos);\
printf(" absVal: %d\n",profile_absVal);\
2018-09-04 19:32:47 +02:00
printf(" perspectiveScale: %d\n",profile_perspectiveScale);\
printf(" wrap: %d\n",profile_wrap);\
printf(" divRoundDown: %d\n",profile_divRoundDown); }
2018-08-31 15:38:02 +02:00
#else
#define profileCall(c)
#endif
2018-08-23 03:04:52 +02:00
Unit clamp(Unit value, Unit valueMin, Unit valueMax)
{
2018-08-31 15:38:02 +02:00
profileCall(clamp);
2018-09-04 19:32:47 +02:00
if (value >= valueMin)
{
if (value <= valueMax)
return value;
else
return valueMax;
}
else
2018-08-23 03:04:52 +02:00
return valueMin;
}
2018-09-03 08:14:36 +02:00
inline Unit absVal(Unit value)
{
2018-09-04 18:55:18 +02:00
profileCall(absVal);
2018-09-04 19:32:47 +02:00
return value >= 0 ? value: -1 * value;
2018-09-03 08:14:36 +02:00
}
2018-09-03 16:55:30 +02:00
/// Like mod, but behaves differently for negative values.
inline Unit wrap(Unit value, Unit mod)
{
2018-09-04 19:32:47 +02:00
profileCall(wrap);
return value >= 0 ? (value % mod) : (mod + (value % mod) - 1);
2018-09-03 16:55:30 +02:00
}
2018-09-03 09:52:50 +02:00
/// Performs division, rounding down, NOT towards zero.
inline Unit divRoundDown(Unit value, Unit divisor)
{
2018-09-04 19:32:47 +02:00
profileCall(divRoundDown);
return value / divisor - ((value >= 0) ? 0 : 1);
2018-09-03 09:52:50 +02:00
}
2018-08-23 09:25:34 +02:00
// Bhaskara's cosine approximation formula
2018-09-02 13:58:46 +02:00
#define trigHelper(x) (((Unit) UNITS_PER_SQUARE) *\
2018-08-23 09:25:34 +02:00
(UNITS_PER_SQUARE / 2 * UNITS_PER_SQUARE / 2 - 4 * (x) * (x)) /\
(UNITS_PER_SQUARE / 2 * UNITS_PER_SQUARE / 2 + (x) * (x)))
Unit cosInt(Unit input)
{
2018-08-31 15:38:02 +02:00
profileCall(cosInt);
2018-09-03 09:52:50 +02:00
// TODO: could be optimized with LUT
2018-09-03 16:55:30 +02:00
input = wrap(input,UNITS_PER_SQUARE);
2018-08-23 09:25:34 +02:00
if (input < UNITS_PER_SQUARE / 4)
return trigHelper(input);
else if (input < UNITS_PER_SQUARE / 2)
return -1 * trigHelper(UNITS_PER_SQUARE / 2 - input);
else if (input < 3 * UNITS_PER_SQUARE / 4)
return -1 * trigHelper(input - UNITS_PER_SQUARE / 2);
else
return trigHelper(UNITS_PER_SQUARE - input);
}
#undef trigHelper
Unit sinInt(Unit input)
{
2018-08-23 09:35:10 +02:00
return cosInt(input - UNITS_PER_SQUARE / 4);
}
Vector2D angleToDirection(Unit angle)
{
2018-08-31 15:38:02 +02:00
profileCall(angleToDirection);
2018-08-23 09:35:10 +02:00
Vector2D result;
result.x = cosInt(angle);
result.y = -1 * sinInt(angle);
return result;
2018-08-23 09:25:34 +02:00
}
2018-09-02 13:58:46 +02:00
uint16_t sqrtInt(uint_maybe32_t value)
2018-08-23 00:50:19 +02:00
{
2018-08-31 15:38:02 +02:00
profileCall(sqrtInt);
2018-09-02 13:58:46 +02:00
uint_maybe32_t result = 0;
2018-09-04 19:35:30 +02:00
uint_maybe32_t a = value;
2018-09-02 13:58:46 +02:00
#ifdef RAYCAST_TINY
uint_maybe32_t b = 1u << 14;
#else
uint_maybe32_t b = 1u << 30;
#endif
2018-08-23 00:50:19 +02:00
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;
}
Unit dist(Vector2D p1, Vector2D p2)
{
2018-08-31 15:38:02 +02:00
profileCall(dist);
2018-09-02 13:58:46 +02:00
int_maybe32_t dx = p2.x - p1.x;
int_maybe32_t dy = p2.y - p1.y;
2018-08-30 11:15:52 +02:00
dx = dx * dx;
dy = dy * dy;
2018-09-02 13:58:46 +02:00
return sqrtInt((uint_maybe32_t) (dx + dy));
2018-08-23 00:50:19 +02:00
}
2018-08-21 13:49:45 +02:00
2018-08-30 14:44:14 +02:00
Unit len(Vector2D v)
{
2018-08-31 15:38:02 +02:00
profileCall(len);
2018-08-30 14:44:14 +02:00
v.x *= v.x;
v.y *= v.y;
2018-09-02 13:58:46 +02:00
return sqrtInt(((uint_maybe32_t) v.x) + ((uint_maybe32_t) v.y));
2018-08-30 14:44:14 +02:00
}
2018-08-21 13:49:45 +02:00
int8_t pointIsLeftOfRay(Vector2D point, Ray ray)
{
2018-08-31 15:38:02 +02:00
profileCall(pointIsLeftOfRay);
2018-08-31 16:46:55 +02:00
Unit dX = point.x - ray.start.x;
Unit dY = point.y - ray.start.y;
2018-08-21 13:49:45 +02:00
return (ray.direction.x * dY - ray.direction.y * dX) > 0;
// ^ Z component of cross-product
}
/**
2018-08-22 08:21:38 +02:00
Casts a ray within a single square, to collide with the square borders.
2018-08-21 13:49:45 +02:00
*/
2018-08-31 13:29:50 +02:00
void castRaySquare(Ray localRay, Vector2D *nextCellOff, Vector2D *collOff)
2018-08-21 13:49:45 +02:00
{
2018-08-31 15:38:02 +02:00
profileCall(castRaySquare);
2018-08-31 13:29:50 +02:00
nextCellOff->x = 0;
nextCellOff->y = 0;
2018-08-21 13:49:45 +02:00
2018-08-22 08:21:38 +02:00
Ray criticalLine = localRay;
2018-08-21 13:49:45 +02:00
#define helper(c1,c2,n)\
{\
2018-08-31 13:29:50 +02:00
nextCellOff->c1 = n;\
collOff->c1 = criticalLine.start.c1 - localRay.start.c1;\
collOff->c2 = \
2018-09-02 13:58:46 +02:00
(((int_maybe32_t) collOff->c1) * localRay.direction.c2) /\
2018-08-30 14:22:13 +02:00
((localRay.direction.c1 == 0) ? 1 : localRay.direction.c1);\
2018-08-21 13:49:45 +02:00
}
#define helper2(n1,n2,c)\
2018-08-22 08:21:38 +02:00
if (pointIsLeftOfRay(localRay.start,criticalLine) == c)\
2018-08-21 13:49:45 +02:00
helper(y,x,n1)\
else\
helper(x,y,n2)
2018-08-22 08:21:38 +02:00
if (localRay.direction.x > 0)
2018-08-21 13:49:45 +02:00
{
2018-08-31 10:59:32 +02:00
criticalLine.start.x = UNITS_PER_SQUARE - 1;
2018-08-21 13:49:45 +02:00
2018-08-22 08:21:38 +02:00
if (localRay.direction.y > 0)
2018-08-21 13:49:45 +02:00
{
// top right
2018-08-31 10:59:32 +02:00
criticalLine.start.y = UNITS_PER_SQUARE - 1;
2018-08-21 13:49:45 +02:00
helper2(1,1,1)
}
else
{
// bottom right
2018-08-31 10:59:32 +02:00
criticalLine.start.y = 0;
2018-08-21 13:49:45 +02:00
helper2(-1,1,0)
}
}
else
{
2018-08-31 10:59:32 +02:00
criticalLine.start.x = 0;
2018-08-21 13:49:45 +02:00
2018-08-22 08:21:38 +02:00
if (localRay.direction.y > 0)
2018-08-21 13:49:45 +02:00
{
// top left
2018-08-31 10:59:32 +02:00
criticalLine.start.y = UNITS_PER_SQUARE - 1;
2018-08-21 13:49:45 +02:00
helper2(1,-1,0)
}
else
{
// bottom left
2018-08-31 10:59:32 +02:00
criticalLine.start.y = 0;
2018-08-21 13:49:45 +02:00
helper2(-1,-1,1)
}
}
#undef helper2
#undef helper
2018-08-31 10:59:32 +02:00
2018-08-31 13:29:50 +02:00
collOff->x += nextCellOff->x;
collOff->y += nextCellOff->y;
2018-08-21 13:49:45 +02:00
}
2018-09-05 16:26:13 +02:00
void castRayMultiHit(Ray ray, ArrayFunction arrayFunc, ArrayFunction typeFunc,
HitResult *hitResults, uint16_t *hitResultsLen, RayConstraints constraints)
2018-08-22 08:21:38 +02:00
{
2018-08-31 15:38:02 +02:00
profileCall(castRayMultiHit);
2018-08-23 00:50:19 +02:00
Vector2D initialPos = ray.start;
2018-08-23 02:36:51 +02:00
Vector2D currentPos = ray.start;
Vector2D currentSquare;
2018-08-23 00:50:19 +02:00
2018-09-03 09:52:50 +02:00
currentSquare.x = divRoundDown(ray.start.x, UNITS_PER_SQUARE);
currentSquare.y = divRoundDown(ray.start.y,UNITS_PER_SQUARE);
2018-09-02 21:43:44 +02:00
2018-08-23 02:36:51 +02:00
*hitResultsLen = 0;
2018-09-05 11:35:52 +02:00
Unit squareType = arrayFunc(currentSquare.x,currentSquare.y);
2018-08-23 00:50:19 +02:00
2018-08-30 14:44:14 +02:00
Vector2D no, co; // next cell offset, collision offset
2018-08-31 13:29:50 +02:00
no.x = 0; // just to supress a warning
no.y = 0;
2018-09-04 18:55:18 +02:00
co.x = 0;
co.y = 0;
2018-08-31 13:29:50 +02:00
2018-08-31 16:46:55 +02:00
for (uint16_t i = 0; i < constraints.maxSteps; ++i)
2018-08-22 08:21:38 +02:00
{
2018-09-05 11:35:52 +02:00
Unit currentType = arrayFunc(currentSquare.x,currentSquare.y);
2018-08-23 02:36:51 +02:00
if (currentType != squareType)
2018-08-23 00:50:19 +02:00
{
2018-08-23 02:36:51 +02:00
// collision
HitResult h;
2018-08-23 00:50:19 +02:00
2018-08-23 02:36:51 +02:00
h.position = currentPos;
h.square = currentSquare;
h.distance = dist(initialPos,currentPos);
2018-09-05 16:26:13 +02:00
if (typeFunc != 0)
h.type = typeFunc(currentSquare.x,currentSquare.y);
2018-08-30 14:44:14 +02:00
if (no.y > 0)
2018-09-03 14:23:11 +02:00
{
2018-08-30 14:44:14 +02:00
h.direction = 0;
2018-09-06 10:15:23 +02:00
h.textureCoord = constraints.computeTextureCoords ?
wrap(currentPos.x,UNITS_PER_SQUARE) : 0;
2018-09-03 14:23:11 +02:00
}
2018-08-30 14:44:14 +02:00
else if (no.x > 0)
2018-09-03 14:23:11 +02:00
{
2018-08-30 14:44:14 +02:00
h.direction = 1;
2018-09-06 10:15:23 +02:00
h.textureCoord = constraints.computeTextureCoords ?
wrap(UNITS_PER_SQUARE - currentPos.y,UNITS_PER_SQUARE) : 0;
2018-09-03 14:23:11 +02:00
}
2018-08-30 14:44:14 +02:00
else if (no.y < 0)
2018-09-03 14:23:11 +02:00
{
2018-08-30 14:44:14 +02:00
h.direction = 2;
2018-09-06 10:15:23 +02:00
h.textureCoord = constraints.computeTextureCoords ?
wrap(UNITS_PER_SQUARE - currentPos.x,UNITS_PER_SQUARE) : 0;
2018-09-03 14:23:11 +02:00
}
2018-08-30 14:44:14 +02:00
else
2018-09-03 14:23:11 +02:00
{
2018-08-30 14:44:14 +02:00
h.direction = 3;
2018-09-06 10:15:23 +02:00
h.textureCoord = constraints.computeTextureCoords ?
wrap(currentPos.y,UNITS_PER_SQUARE) : 0;
2018-09-03 14:23:11 +02:00
}
2018-08-30 14:44:14 +02:00
2018-08-23 02:36:51 +02:00
hitResults[*hitResultsLen] = h;
*hitResultsLen += 1;
squareType = currentType;
2018-08-31 13:29:50 +02:00
if (*hitResultsLen >= constraints.maxHits)
2018-08-23 02:36:51 +02:00
break;
2018-08-23 00:50:19 +02:00
}
2018-09-03 16:55:30 +02:00
ray.start.x = wrap(currentPos.x,UNITS_PER_SQUARE);
ray.start.y = wrap(currentPos.y,UNITS_PER_SQUARE);
2018-08-22 08:21:38 +02:00
castRaySquare(ray,&no,&co);
2018-08-23 02:36:51 +02:00
currentSquare.x += no.x;
currentSquare.y += no.y;
2018-08-22 08:21:38 +02:00
2018-08-31 11:20:36 +02:00
// offset into the next cell
2018-08-23 02:36:51 +02:00
currentPos.x += co.x;
currentPos.y += co.y;
2018-08-22 08:21:38 +02:00
}
2018-08-23 02:36:51 +02:00
}
2018-08-31 13:29:50 +02:00
HitResult castRay(Ray ray, ArrayFunction arrayFunc)
2018-08-23 02:36:51 +02:00
{
2018-08-31 15:38:02 +02:00
profileCall(castRay);
2018-08-23 02:36:51 +02:00
HitResult result;
uint16_t len;
2018-08-31 13:29:50 +02:00
RayConstraints c;
c.maxSteps = 1000;
c.maxHits = 1;
2018-08-23 02:36:51 +02:00
2018-09-05 16:26:13 +02:00
castRayMultiHit(ray,arrayFunc,0,&result,&len,c);
2018-08-23 02:36:51 +02:00
if (len == 0)
result.distance = -1;
2018-08-22 08:21:38 +02:00
return result;
}
2018-08-31 18:11:32 +02:00
void castRaysMultiHit(Camera cam, ArrayFunction arrayFunc,
2018-09-05 16:26:13 +02:00
ArrayFunction typeFunction, ColumnFunction columnFunc,
RayConstraints constraints)
2018-08-30 08:51:53 +02:00
{
2018-08-31 16:46:55 +02:00
uint16_t fovHalf = cam.fovAngle / 2;
2018-08-30 08:51:53 +02:00
2018-08-31 13:29:50 +02:00
Vector2D dir1 = angleToDirection(cam.direction - fovHalf);
Vector2D dir2 = angleToDirection(cam.direction + fovHalf);
2018-08-30 08:51:53 +02:00
Unit dX = dir2.x - dir1.x;
Unit dY = dir2.y - dir1.y;
2018-08-31 13:29:50 +02:00
HitResult hits[constraints.maxHits];
2018-08-31 11:51:03 +02:00
uint16_t hitCount;
2018-08-30 08:51:53 +02:00
Ray r;
2018-08-31 13:29:50 +02:00
r.start = cam.position;
2018-08-30 08:51:53 +02:00
2018-08-31 18:11:32 +02:00
for (uint16_t i = 0; i < cam.resolution.x; ++i)
2018-08-30 08:51:53 +02:00
{
2018-08-31 13:29:50 +02:00
r.direction.x = dir1.x + (dX * i) / cam.resolution.x;
r.direction.y = dir1.y + (dY * i) / cam.resolution.x;
2018-08-30 08:51:53 +02:00
2018-09-05 16:26:13 +02:00
castRayMultiHit(r,arrayFunc,typeFunction,hits,&hitCount,constraints);
2018-08-30 08:51:53 +02:00
2018-08-31 18:11:32 +02:00
columnFunc(hits,hitCount,i,r);
2018-08-30 08:51:53 +02:00
}
}
2018-09-05 11:35:52 +02:00
// global helper variables, for precomputing stuff etc.
2018-08-31 18:26:51 +02:00
PixelFunction _pixelFunction = 0;
Camera _camera;
2018-09-01 12:04:29 +02:00
Unit _floorDepthStep = 0;
2018-09-05 11:35:52 +02:00
Unit _startFloorHeight = 0;
Unit _startCeilHeight = 0;
2018-09-02 13:58:46 +02:00
int_maybe32_t _camResYLimit = 0;
2018-09-03 13:06:45 +02:00
Unit _middleRow = 0;
2018-09-05 11:35:52 +02:00
ArrayFunction _floorFunction = 0;
ArrayFunction _ceilFunction = 0;
2018-09-05 11:44:15 +02:00
uint8_t _computeTextureCoords = 0;
2018-09-05 11:35:52 +02:00
/// Helper function that determines intersection with both ceiling and floor.
Unit _floorCeilFunction(int16_t x, int16_t y)
{
// TODO: adjust also for RAYCAST_TINY
Unit f = _floorFunction(x,y);
2018-09-05 12:20:46 +02:00
Unit c = _ceilFunction != 0 ? _ceilFunction(x,y) : 0;
2018-09-05 11:35:52 +02:00
return ((f & 0x0000ffff) << 16) | (c & 0x0000ffff);
}
2018-08-31 18:26:51 +02:00
void _columnFunction(HitResult *hits, uint16_t hitCount, uint16_t x, Ray ray)
{
2018-09-03 08:14:36 +02:00
int_maybe32_t y = _camResYLimit; // screen y (for floor), will only go up
int_maybe32_t y2 = 0; // screen y (for ceil), will only fo down
2018-09-02 23:26:02 +02:00
2018-09-05 11:35:52 +02:00
Unit worldZPrev = _startFloorHeight;
Unit worldZPrevCeil = _startCeilHeight;
2018-09-01 09:46:19 +02:00
2018-09-01 12:04:29 +02:00
PixelInfo p;
p.position.x = x;
2018-09-03 09:39:46 +02:00
#define VERTICAL_DEPTH_MULTIPLY 2
2018-09-03 08:14:36 +02:00
// we'll be simulatenously drawing the floor and the ceiling now
2018-09-02 13:58:46 +02:00
for (uint_maybe32_t j = 0; j < hitCount; ++j)
2018-08-31 19:13:15 +02:00
{
HitResult hit = hits[j];
2018-08-31 18:26:51 +02:00
2018-09-01 07:27:17 +02:00
/* 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). */
2018-08-31 19:13:15 +02:00
Unit dist = // adjusted distance
(hit.distance * vectorsAngleCos(angleToDirection(_camera.direction),
ray.direction)) / UNITS_PER_SQUARE;
2018-08-31 18:26:51 +02:00
2018-08-31 19:13:15 +02:00
dist = dist == 0 ? 1 : dist; // prevent division by zero
2018-08-31 18:26:51 +02:00
2018-09-05 11:35:52 +02:00
Unit wallHeight = _floorFunction(hit.square.x,hit.square.y);
2018-09-05 12:20:46 +02:00
Unit wallHeightCeil = _ceilFunction != 0 ?
_ceilFunction(hit.square.x,hit.square.y) : 0;
2018-08-31 18:26:51 +02:00
2018-09-02 23:26:02 +02:00
Unit worldZ2 = wallHeight - _camera.height;
2018-09-05 11:35:52 +02:00
Unit worldZ2Ceil = wallHeightCeil - _camera.height;
2018-08-31 19:01:36 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t z1Screen = _middleRow - perspectiveScale(
2018-09-03 16:38:49 +02:00
(worldZPrev * _camera.resolution.y) / UNITS_PER_SQUARE,dist);
2018-09-01 12:04:29 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t z1ScreenNoClamp = z1Screen;
2018-09-01 13:39:06 +02:00
z1Screen = clamp(z1Screen,0,_camResYLimit);
2018-08-31 19:01:36 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t z1ScreenCeil = _middleRow - perspectiveScale(
2018-09-03 16:38:49 +02:00
(worldZPrevCeil * _camera.resolution.y) / UNITS_PER_SQUARE,dist);
2018-09-02 23:26:02 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t z1ScreenCeilNoClamp = z1ScreenCeil;
2018-09-03 08:14:36 +02:00
z1ScreenCeil = clamp(z1ScreenCeil,0,_camResYLimit);
2018-09-02 23:26:02 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t z2Screen = _middleRow - perspectiveScale(
2018-09-03 16:38:49 +02:00
(worldZ2 * _camera.resolution.y) / UNITS_PER_SQUARE,dist);
2018-09-01 08:17:01 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t wallScreenHeightNoClamp = z2Screen - z1ScreenNoClamp;
2018-09-05 19:49:39 +02:00
wallScreenHeightNoClamp = wallScreenHeightNoClamp != 0 ?
wallScreenHeightNoClamp : 1;
2018-09-01 13:39:06 +02:00
z2Screen = clamp(z2Screen,0,_camResYLimit);
2018-09-01 12:04:29 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t z2ScreenCeil = _middleRow - perspectiveScale(
2018-09-03 16:38:49 +02:00
(worldZ2Ceil * _camera.resolution.y) / UNITS_PER_SQUARE,dist);
2018-09-02 23:26:02 +02:00
2018-09-05 11:44:15 +02:00
int_maybe32_t wallScreenHeightCeilNoClamp =
z2ScreenCeil - z1ScreenCeilNoClamp;
2018-09-03 14:23:11 +02:00
2018-09-05 19:49:39 +02:00
wallScreenHeightCeilNoClamp = wallScreenHeightCeilNoClamp != 0 ?
wallScreenHeightCeilNoClamp : 1;
2018-09-03 08:14:36 +02:00
z2ScreenCeil = clamp(z2ScreenCeil,0,_camResYLimit);
2018-09-02 23:26:02 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t zTop = z1Screen < z2Screen ? z1Screen : z2Screen;
2018-09-01 08:17:01 +02:00
2018-09-03 14:23:11 +02:00
int_maybe32_t zBottomCeil = z1ScreenCeil > z2ScreenCeil ?
2018-09-03 08:14:36 +02:00
z1ScreenCeil : z2ScreenCeil;
2018-09-02 23:26:02 +02:00
2018-09-03 08:14:36 +02:00
if (zTop <= zBottomCeil)
zBottomCeil = zTop; // walls on ceiling and floor met
2018-09-02 23:26:02 +02:00
2018-09-03 08:14:36 +02:00
// draw floor until wall
2018-09-01 09:55:35 +02:00
2018-09-01 08:17:01 +02:00
p.isWall = 0;
2018-09-05 19:08:38 +02:00
p.isFloor = 1;
2018-08-31 18:26:51 +02:00
2018-09-03 09:39:46 +02:00
Unit floorCameraDiff = absVal(worldZPrev) * VERTICAL_DEPTH_MULTIPLY;
2018-09-01 09:55:35 +02:00
2018-09-02 23:26:02 +02:00
for (int_maybe32_t i = y; i > z1Screen; --i)
2018-08-31 19:13:15 +02:00
{
2018-09-01 08:17:01 +02:00
p.position.y = i;
2018-09-03 08:14:36 +02:00
p.depth = (_camera.resolution.y - i) * _floorDepthStep + floorCameraDiff;
2018-09-01 08:17:01 +02:00
_pixelFunction(p);
}
2018-09-05 19:49:39 +02:00
if (z1Screen < y)
y = z1Screen;
2018-09-03 08:14:36 +02:00
// draw ceiling until wall
2018-09-02 23:26:02 +02:00
2018-09-05 19:08:38 +02:00
p.isFloor = 0;
2018-09-05 12:20:46 +02:00
if (_ceilFunction != 0)
2018-09-03 08:14:36 +02:00
{
2018-09-05 12:20:46 +02:00
Unit ceilCameraDiff = absVal(worldZPrevCeil) * VERTICAL_DEPTH_MULTIPLY;
for (int_maybe32_t i = y2; i < z1ScreenCeil; ++i)
{
p.position.y = i;
p.depth = i * _floorDepthStep + ceilCameraDiff;
_pixelFunction(p);
}
2018-09-03 08:14:36 +02:00
}
2018-09-02 23:26:02 +02:00
2018-09-05 19:49:39 +02:00
if (z1ScreenCeil > y2)
y2 = z1ScreenCeil;
2018-09-05 07:07:12 +02:00
// draw floor wall
2018-09-01 08:17:01 +02:00
p.isWall = 1;
2018-09-01 12:04:29 +02:00
p.depth = dist;
2018-09-05 19:08:38 +02:00
p.isFloor = 1;
2018-09-01 08:17:01 +02:00
2018-09-05 07:07:12 +02:00
int_maybe32_t iTo = y2 < zTop ? zTop : y2;
2018-09-05 18:38:53 +02:00
for (int_maybe32_t i = z1Screen < y ? z1Screen : y; i >= iTo; --i)
2018-09-01 08:17:01 +02:00
{
2018-08-31 19:13:15 +02:00
p.position.y = i;
p.hit = hit;
2018-09-05 11:44:15 +02:00
if (_computeTextureCoords)
2018-09-06 09:57:38 +02:00
p.textureCoordY = UNITS_PER_SQUARE - 1 -((i - z1ScreenNoClamp) *
2018-09-06 08:55:03 +02:00
UNITS_PER_SQUARE) / wallScreenHeightNoClamp;
2018-09-05 11:44:15 +02:00
2018-08-31 19:13:15 +02:00
_pixelFunction(p);
}
2018-09-01 09:46:19 +02:00
2018-09-03 08:14:36 +02:00
// draw ceiling wall
2018-09-05 19:08:38 +02:00
p.isFloor = 0;
2018-09-05 12:20:46 +02:00
if (_ceilFunction != 0)
2018-09-03 08:14:36 +02:00
{
2018-09-05 12:20:46 +02:00
iTo = y > zBottomCeil ? zBottomCeil : y;
2018-09-05 11:44:15 +02:00
2018-09-05 18:38:53 +02:00
for (int_maybe32_t i = z1ScreenCeil > y2 ? z1ScreenCeil : y2; i <= iTo;
2018-09-05 12:20:46 +02:00
++i)
{
p.position.y = i;
p.hit = hit;
2018-09-05 11:44:15 +02:00
2018-09-05 12:20:46 +02:00
if (_computeTextureCoords)
2018-09-06 09:57:38 +02:00
p.textureCoordY = UNITS_PER_SQUARE - 1 - ((i - z1ScreenCeilNoClamp) *
2018-09-06 08:55:03 +02:00
UNITS_PER_SQUARE) / wallScreenHeightCeilNoClamp;
2018-09-05 12:20:46 +02:00
_pixelFunction(p);
}
2018-09-03 08:14:36 +02:00
}
2018-09-02 23:26:02 +02:00
2018-09-01 09:55:35 +02:00
y = y > zTop ? zTop : y;
2018-09-01 09:46:19 +02:00
worldZPrev = worldZ2;
2018-09-02 23:26:02 +02:00
2018-09-03 08:14:36 +02:00
y2 = y2 < zBottomCeil ? zBottomCeil : y2;
worldZPrevCeil = worldZ2Ceil;
2018-09-02 23:26:02 +02:00
2018-09-03 08:14:36 +02:00
if (y <= y2)
break; // walls on ceiling and floor met
2018-09-01 12:04:29 +02:00
}
// draw floor until horizon
p.isWall = 0;
2018-09-05 19:08:38 +02:00
p.isFloor = 1;
2018-09-01 12:04:29 +02:00
2018-09-03 09:39:46 +02:00
Unit floorCameraDiff = absVal(worldZPrev) * VERTICAL_DEPTH_MULTIPLY;
2018-09-05 12:20:46 +02:00
Unit horizon = (y2 < _middleRow || _ceilFunction == 0) ? _middleRow : y2;
2018-09-02 23:26:02 +02:00
2018-09-05 18:38:53 +02:00
for (int_maybe32_t i = y; i >= horizon + (horizon > y2 ? 0 : 1); --i)
2018-09-01 12:04:29 +02:00
{
p.position.y = i;
2018-09-03 08:14:36 +02:00
p.depth = (_camera.resolution.y - i) * _floorDepthStep + floorCameraDiff;
2018-09-02 13:58:46 +02:00
2018-09-01 12:10:45 +02:00
_pixelFunction(p);
2018-08-31 18:26:51 +02:00
}
2018-09-02 23:26:02 +02:00
2018-09-03 08:14:36 +02:00
// draw ceiling until horizon
2018-09-02 23:26:02 +02:00
2018-09-05 12:20:46 +02:00
if (_ceilFunction != 0)
2018-09-03 08:14:36 +02:00
{
2018-09-05 19:08:38 +02:00
p.isFloor = 0;
2018-09-05 12:20:46 +02:00
Unit ceilCameraDiff = absVal(worldZPrevCeil) * VERTICAL_DEPTH_MULTIPLY;
horizon = y > _middleRow ? _middleRow : y;
2018-09-03 08:14:36 +02:00
2018-09-05 12:20:46 +02:00
for (int_maybe32_t i = y2; i < horizon; ++i)
{
p.position.y = i;
p.depth = i * _floorDepthStep + ceilCameraDiff;
_pixelFunction(p);
}
2018-09-03 08:14:36 +02:00
}
2018-09-02 23:26:02 +02:00
2018-09-03 09:39:46 +02:00
#undef VERTICAL_DEPTH_MULTIPLY
2018-08-31 18:26:51 +02:00
}
2018-09-05 16:26:13 +02:00
void render(Camera cam, ArrayFunction floorHeightFunc,
ArrayFunction ceilingHeightFunc, ArrayFunction typeFunction,
PixelFunction pixelFunc, RayConstraints constraints)
2018-08-31 18:26:51 +02:00
{
_pixelFunction = pixelFunc;
2018-09-05 11:35:52 +02:00
_floorFunction = floorHeightFunc;
_ceilFunction = ceilingHeightFunc;
2018-08-31 18:26:51 +02:00
_camera = cam;
2018-09-01 13:39:06 +02:00
_camResYLimit = cam.resolution.y - 1;
_middleRow = cam.resolution.y / 2;
2018-09-05 11:44:15 +02:00
_computeTextureCoords = constraints.computeTextureCoords;
2018-09-01 12:04:29 +02:00
2018-09-05 11:35:52 +02:00
_startFloorHeight = floorHeightFunc(
divRoundDown(cam.position.x,UNITS_PER_SQUARE),
divRoundDown(cam.position.y,UNITS_PER_SQUARE)) -1 * cam.height;
2018-09-05 12:20:46 +02:00
_startCeilHeight =
ceilingHeightFunc != 0 ?
ceilingHeightFunc(
divRoundDown(cam.position.x,UNITS_PER_SQUARE),
divRoundDown(cam.position.y,UNITS_PER_SQUARE)) -1 * cam.height
: UNIT_INFINITY;
2018-09-01 12:04:29 +02:00
// TODO
2018-09-02 13:58:46 +02:00
_floorDepthStep = (12 * UNITS_PER_SQUARE) / cam.resolution.y;
2018-09-01 12:04:29 +02:00
2018-09-05 16:26:13 +02:00
castRaysMultiHit(cam,_floorCeilFunction,typeFunction,
_columnFunction,constraints);
2018-08-31 18:26:51 +02:00
}
2018-08-30 18:31:08 +02:00
Vector2D normalize(Vector2D v)
{
2018-08-31 15:38:02 +02:00
profileCall(normalize);
2018-08-30 18:31:08 +02:00
Vector2D result;
Unit l = len(v);
2018-09-04 19:35:30 +02:00
l = l != 0 ? l : 1;
2018-09-02 13:58:46 +02:00
2018-08-30 18:31:08 +02:00
result.x = (v.x * UNITS_PER_SQUARE) / l;
result.y = (v.y * UNITS_PER_SQUARE) / l;
return result;
}
Unit vectorsAngleCos(Vector2D v1, Vector2D v2)
{
2018-08-31 15:38:02 +02:00
profileCall(vectorsAngleCos);
2018-08-30 18:31:08 +02:00
v1 = normalize(v1);
v2 = normalize(v2);
return (v1.x * v2.x + v1.y * v2.y) / UNITS_PER_SQUARE;
}
2018-09-04 11:42:27 +02:00
PixelInfo mapToScreen(Vector2D worldPosition, Unit height, Camera camera)
{
// TODO: precompute some stuff that's constant in the frame
PixelInfo result;
Unit d = dist(worldPosition,camera.position);
Vector2D toPoint;
toPoint.x = worldPosition.x - camera.position.x;
toPoint.y = worldPosition.y - camera.position.y;
Vector2D cameraDir = angleToDirection(camera.direction);
result.depth = // adjusted distance
(d * vectorsAngleCos(cameraDir,toPoint)) / UNITS_PER_SQUARE;
result.position.y = camera.resolution.y / 2 -
(camera.resolution.y *
perspectiveScale(height - camera.height,result.depth)) / UNITS_PER_SQUARE;
Unit middleColumn = camera.resolution.x / 2;
Unit a = sqrtInt(d * d - result.depth * result.depth);
Ray r;
r.start = camera.position;
r.direction = cameraDir;
if (!pointIsLeftOfRay(worldPosition,r))
a *= -1;
Unit alpha = camera.fovAngle / 2;
Unit cos = cosInt(alpha);
Unit b =
(result.depth * sinInt(alpha)) / (cos == 0 ? 1 : cos); // sin/cos = tan
result.position.x = (a * middleColumn) / b;
2018-09-04 13:38:04 +02:00
result.position.x = middleColumn - result.position.x;
2018-09-04 11:42:27 +02:00
return result;
}
2018-08-30 08:51:53 +02:00
Unit degreesToUnitsAngle(int16_t degrees)
{
return (degrees * UNITS_PER_SQUARE) / 360;
}
2018-09-03 16:38:49 +02:00
Unit perspectiveScale(Unit originalSize, Unit distance)
2018-08-30 08:51:53 +02:00
{
2018-09-04 18:55:18 +02:00
profileCall(perspectiveScale);
2018-09-04 19:32:47 +02:00
return distance != 0 ?
(originalSize * UNITS_PER_SQUARE) /
((VERTICAL_FOV * 2 * distance) / UNITS_PER_SQUARE)
: 0;
2018-08-30 08:51:53 +02:00
}
2018-08-23 02:46:40 +02:00
#endif