diff options
Diffstat (limited to 'src/raycastlib.cpp')
| -rw-r--r-- | src/raycastlib.cpp | 1563 | 
1 files changed, 1563 insertions, 0 deletions
diff --git a/src/raycastlib.cpp b/src/raycastlib.cpp new file mode 100644 index 0000000..07f1b87 --- /dev/null +++ b/src/raycastlib.cpp @@ -0,0 +1,1563 @@ +#include "raycastlib.h" + +//============================================================================= +// privates + +#define _RCL_UNUSED(what) (void)(what); + +#ifndef RCL_PIXEL_FUNCTION +#define RCL_PIXEL_FUNCTION pixelFunc +#endif +void pixelFunc(RCL_PixelInfo *p); + +// 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; +RCL_Unit _RCL_fovCorrectionFactors[2] = {0,0}; //correction for hor/vert fov + +RCL_Unit RCL_clamp(RCL_Unit value, RCL_Unit valueMin, RCL_Unit valueMax) +{ +  if (value >= valueMin) +  { +    if (value <= valueMax) +      return value; +    else +      return valueMax; +  } +  else +    return valueMin; +} + +static inline RCL_Unit RCL_abs(RCL_Unit value) +{ +  return value * (((value >= 0) << 1) - 1); +} + +/// Like mod, but behaves differently for negative values. +static inline RCL_Unit RCL_wrap(RCL_Unit value, RCL_Unit mod) +{ +  RCL_Unit cmp = value < 0; +  return cmp * mod + (value % mod) - cmp; +} + +/// Performs division, rounding down, NOT towards zero. +RCL_Unit RCL_divRoundDown(RCL_Unit value, RCL_Unit divisor) +{ +  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_cos(RCL_Unit input) +{ +  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_sin(RCL_Unit input) +{ +  return RCL_cos(input - RCL_UNITS_PER_SQUARE / 4); +} + +RCL_Unit RCL_tan(RCL_Unit input) +{ +  return (RCL_sin(input) * RCL_UNITS_PER_SQUARE) / RCL_nonZero(RCL_cos(input) +); + +  return (RCL_sin(input) * RCL_UNITS_PER_SQUARE) / RCL_nonZero(RCL_cos(input)); +} + +RCL_Unit RCL_ctg(RCL_Unit input) +{ +  return (RCL_cos(input) * RCL_UNITS_PER_SQUARE) / RCL_sin(input); +} + +RCL_Vector2D RCL_angleToDirection(RCL_Unit angle) +{ +  RCL_Vector2D result; + +  result.x = RCL_cos(angle); +  result.y = -1 * RCL_sin(angle); + +  return result; +} + +uint16_t RCL_sqrt(RCL_Unit value) +{ +#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_Unit dx = p2.x - p1.x; +  RCL_Unit dy = p2.y - p1.y; + +#if RCL_USE_DIST_APPROX == 2 +  // octagonal approximation + +  dx = RCL_abs(dx); +  dy = RCL_abs(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) * 2 - 1) * dx; +  dy = ((dy < 0) * 2 - 1) * 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_sqrt((RCL_Unit) (dx + dy)); +#endif +} + +RCL_Unit RCL_len(RCL_Vector2D v) +{ +  RCL_Vector2D zero; +  zero.x = 0; +  zero.y = 0; + +  return RCL_dist(zero,v); +} + +static inline int8_t RCL_pointIsLeftOfRay(RCL_Vector2D point, RCL_Ray ray) +{ +  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_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_abs(dirVecLengthNorm / RCL_nonZero(ray.direction.x)); +  delta.y = RCL_abs(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 + +  #define RECIP_SCALE 65536 + +  RCL_Unit rayDirXRecip = RECIP_SCALE / RCL_nonZero(ray.direction.x); +  RCL_Unit rayDirYRecip = RECIP_SCALE / RCL_nonZero(ray.direction.y); +  // ^ we precompute reciprocals to avoid divisions in the loop + +  for (uint16_t i = 0; i < constraints.maxSteps; ++i) +  { +    RCL_Unit currentType = arrayFunc(currentSquare.x,currentSquare.y); + +    if (RCL_unlikely(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 = // avoid division by multiplying with reciprocal +          ray.start.y + (ray.direction.y * diff * rayDirXRecip) / RECIP_SCALE; + +#if RCL_RECTILINEAR +        /* Here we compute the fish eye corrected distance (perpendicular to +        the projection plane) as the Euclidean distance (of hit from camera +        position) 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 principal axis) divided by leg B (length along +        the same principal axis). */ + +#define CORRECT(dir1,dir2)\ +  RCL_Unit tmp = diff / 4;        /* 4 to prevent overflow */ \ +  h.distance = ((tmp / 8) != 0) ? /* prevent a bug with small dists */ \ +    ((tmp * RCL_UNITS_PER_SQUARE * rayDir ## dir1 ## Recip) / (RECIP_SCALE / 4)):\ +    RCL_abs(h.position.dir2 - ray.start.dir2); + +        CORRECT(X,y) + +#endif // RCL_RECTILINEAR +      } +      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 * rayDirYRecip) / RECIP_SCALE; + +#if RCL_RECTILINEAR + +        CORRECT(Y,x) // same as above but for different axis + +#undef CORRECT + +#endif // RCL_RECTILINEAR +      } + +#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_HitResult result; +  uint16_t len; +  RCL_RayConstraints c; + +  c.maxSteps = 1000; +  c.maxHits = 1; + +  RCL_castRayMultiHit(ray,arrayFunc,0,&result,&len,c); + +  if (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); + +  /* We scale the side distances so that the middle one is +     RCL_UNITS_PER_SQUARE, which has to be this way. */ + +  RCL_Unit cos = RCL_nonZero(RCL_cos(RCL_HORIZONTAL_FOV_HALF)); + +  dir1.x = (dir1.x * RCL_UNITS_PER_SQUARE) / cos; +  dir1.y = (dir1.y * RCL_UNITS_PER_SQUARE) / cos; + +  dir2.x = (dir2.x * RCL_UNITS_PER_SQUARE) / cos; +  dir2.y = (dir2.y * RCL_UNITS_PER_SQUARE) / cos; + +  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_drawHorizontalColumn( +  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_UNUSED(ray); + +  RCL_Unit depthIncrement; +  RCL_Unit dx; +  RCL_Unit dy; + +  pixelInfo->isWall = 0; + +  int16_t limit = RCL_clamp(yTo,limit1,limit2); + +  RCL_Unit depth = 0; /* TODO: this is for clamping depth to 0 so that we don't +                         have negative depths, but we should do it more +                         elegantly and efficiently */ + +  _RCL_UNUSED(depth); + +  /* 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*/\ +    {\ +      depth = pixelInfo->depth + RCL_abs(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*/\ +      {\ +        depth += depthIncrement;\ +        pixelInfo->depth = RCL_zeroClamp(depth); \ +        /* ^ int comparison is fast, it is not braching! (= test instr.) */\ +      }\ +      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 +  ) +{ +  _RCL_UNUSED(height) + +  height = RCL_abs(height); + +  pixelInfo->isWall = 1; + +  RCL_Unit limit = RCL_clamp(yTo,limit1,limit2); + +  RCL_Unit wallLength = RCL_nonZero(RCL_abs(yTo - yFrom - 1)); + +  RCL_Unit wallPosition = RCL_abs(yFrom - yCurrent) - increment; + +  RCL_Unit heightScaled = height * RCL_TEXTURE_INTERPOLATION_SCALE; +  _RCL_UNUSED(heightScaled); + +  RCL_Unit coordStepScaled = RCL_COMPUTE_WALL_TEXCOORDS ? +#if RCL_TEXTURE_VERTICAL_STRETCH == 1 +    ((RCL_UNITS_PER_SQUARE * RCL_TEXTURE_INTERPOLATION_SCALE) / wallLength) +#else +    (heightScaled / wallLength) +#endif +    : 0; + +  pixelInfo->texCoords.y = RCL_COMPUTE_WALL_TEXCOORDS ? +    (wallPosition * coordStepScaled) : 0; + +  if (increment < 0) +  { +    coordStepScaled *= -1; +    pixelInfo->texCoords.y = +#if RCL_TEXTURE_VERTICAL_STRETCH == 1 +      (RCL_UNITS_PER_SQUARE * RCL_TEXTURE_INTERPOLATION_SCALE) +      - pixelInfo->texCoords.y; +#else +      heightScaled - pixelInfo->texCoords.y; +#endif +  } +  else +  { +    // with floor wall, don't start under 0 +    pixelInfo->texCoords.y = RCL_zeroClamp(pixelInfo->texCoords.y); +  } + +  RCL_Unit textureCoordScaled = pixelInfo->texCoords.y; + +  for (RCL_Unit i = yCurrent + increment;  +       increment == -1 ? i >= limit : i <= limit; // TODO: is efficient? +       i += increment) +  { +    pixelInfo->position.y = i; + +#if RCL_COMPUTE_WALL_TEXCOORDS == 1 +    pixelInfo->texCoords.y = +      textureCoordScaled / RCL_TEXTURE_INTERPOLATION_SCALE; + +    textureCoordScaled += coordStepScaled; +#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.wallHeight = 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_perspectiveScaleVertical( +        (fZ1World * _RCL_camera.resolution.y) / +        RCL_UNITS_PER_SQUARE,distance); +      fZ2Screen = _RCL_middleRow - RCL_perspectiveScaleVertical( +        (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_perspectiveScaleVertical( +          (cZ1World * _RCL_camera.resolution.y) / +          RCL_UNITS_PER_SQUARE,distance); +        cZ2Screen = _RCL_middleRow - RCL_perspectiveScaleVertical( +          (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; +    p.wallHeight = 0; + +#if RCL_COMPUTE_FLOOR_DEPTH == 1 +    p.depth = (_RCL_fHorizontalDepthStart - fPosY) * _RCL_horizontalDepthStep; +#else +    p.depth = 0; +#endif + +    limit = _RCL_drawHorizontalColumn(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_drawHorizontalColumn(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 = fZ1World + _RCL_camera.height; +      p.wallHeight = fWallHeight; + +      // 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; +        p.height = cZ1World + _RCL_camera.height; +        p.wallHeight = cWallHeight; + +        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 +                  cZ1World - cZ2World  +#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 dist = 1; + +  RCL_PixelInfo p; +  p.position.x = x; +  p.wallHeight = RCL_UNITS_PER_SQUARE; + +  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; + +      RCL_Unit wallHeightWorld = _RCL_floorFunction(hit.square.x,hit.square.y); + +      if (wallHeightWorld < 0) +      { +        /* We can't just do wallHeightWorld = max(0,wallHeightWorld) because +        we would be processing an actual hit with height 0, which shouldn't +        ever happen, so we assign some arbitrary height. */ + +        wallHeightWorld = RCL_UNITS_PER_SQUARE; +      } + +      RCL_Unit worldPointTop = wallHeightWorld - _RCL_camera.height; +      RCL_Unit worldPointBottom = -1 * _RCL_camera.height; + +      wallStart = _RCL_middleRow -   +        (RCL_perspectiveScaleVertical(worldPointTop,dist) +        * _RCL_camera.resolution.y) / RCL_UNITS_PER_SQUARE; + +      int16_t wallEnd =  _RCL_middleRow - +        (RCL_perspectiveScaleVertical(worldPointBottom,dist) +        * _RCL_camera.resolution.y) / RCL_UNITS_PER_SQUARE; + +      wallHeightScreen = wallEnd - wallStart; + +      if (wallHeightScreen <= 0) // can happen because of rounding errors +        wallHeightScreen = 1;  +    } +  } +  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_drawHorizontalColumn(-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_drawHorizontalColumn(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_perspectiveScaleVerticalInverse(camHeightScreenSize, +             RCL_abs(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_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) +{ +  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_Vector2D toPoint; + +  toPoint.x = worldPosition.x - camera.position.x; +  toPoint.y = worldPosition.y - camera.position.y; + +  RCL_Unit middleColumn = camera.resolution.x / 2; + +  // rotate the point to camera space (y left/right, x forw/backw) + +  RCL_Unit cos = RCL_cos(camera.direction); +  RCL_Unit sin = RCL_sin(camera.direction); + +  RCL_Unit tmp = toPoint.x; + +  toPoint.x = (toPoint.x * cos - toPoint.y * sin) / RCL_UNITS_PER_SQUARE;  +  toPoint.y = (tmp * sin + toPoint.y * cos) / RCL_UNITS_PER_SQUARE;  + +  result.depth = toPoint.x; + +  result.position.x = middleColumn - +   (RCL_perspectiveScaleHorizontal(toPoint.y,result.depth) * middleColumn) / +   RCL_UNITS_PER_SQUARE; + +  result.position.y = +    (RCL_perspectiveScaleVertical(height - camera.height,result.depth) +     * camera.resolution.y) / RCL_UNITS_PER_SQUARE; +   +  result.position.y = camera.resolution.y / 2 - result.position.y + camera.shear; + +  return result; +} + +RCL_Unit RCL_degreesToUnitsAngle(int16_t degrees) +{ +  return (degrees * RCL_UNITS_PER_SQUARE) / 360; +} +   +/** +  Ugly temporary hack to solve mapping to screen. This function computes +  (approximately, usin a table) a divisor needed for FOV correction. +*/ +RCL_Unit _RCL_fovCorrectionFactor(RCL_Unit fov) +{ +  uint16_t table[9] =  +    {1,208,408,692,1024,1540,2304,5376,30000}; + +  fov = RCL_min(RCL_UNITS_PER_SQUARE / 2 - 1,fov); + +  uint8_t index = fov / 64; +  uint32_t t = ((fov - index * 64) * RCL_UNITS_PER_SQUARE) / 64;  +  uint32_t v1 = table[index]; +  uint32_t v2 = table[index + 1]; +  +  return v1 + ((v2 - v1) * t) / RCL_UNITS_PER_SQUARE; +} + +RCL_Unit RCL_perspectiveScaleVertical(RCL_Unit originalSize, RCL_Unit distance) +{ +  if (_RCL_fovCorrectionFactors[1] == 0) +    _RCL_fovCorrectionFactors[1] = _RCL_fovCorrectionFactor(RCL_VERTICAL_FOV); + +  return distance != 0 ? ((originalSize * RCL_UNITS_PER_SQUARE) / +   RCL_nonZero((_RCL_fovCorrectionFactors[1] * distance) / RCL_UNITS_PER_SQUARE) +   ) : 0; +} + +RCL_Unit RCL_perspectiveScaleVerticalInverse(RCL_Unit originalSize, +  RCL_Unit scaledSize) +{ +  if (_RCL_fovCorrectionFactors[1] == 0) +    _RCL_fovCorrectionFactors[1] = _RCL_fovCorrectionFactor(RCL_VERTICAL_FOV); + +  return scaledSize != 0 ? + +  ((originalSize * RCL_UNITS_PER_SQUARE) / +   RCL_nonZero((_RCL_fovCorrectionFactors[1] * scaledSize)  +    / RCL_UNITS_PER_SQUARE)) : RCL_INFINITY; +} + +RCL_Unit +  RCL_perspectiveScaleHorizontal(RCL_Unit originalSize, RCL_Unit distance) +{ +  if (_RCL_fovCorrectionFactors[0] == 0) +    _RCL_fovCorrectionFactors[0] = _RCL_fovCorrectionFactor(RCL_HORIZONTAL_FOV); + +  return distance != 0 ? +   ((originalSize * RCL_UNITS_PER_SQUARE) / +   RCL_nonZero((_RCL_fovCorrectionFactors[0] * distance) / RCL_UNITS_PER_SQUARE) +   ) : 0; +} + +RCL_Unit RCL_perspectiveScaleHorizontalInverse(RCL_Unit originalSize, +  RCL_Unit scaledSize) +{ +  // TODO: probably doesn't work + +  return scaledSize != 0 ? +    (originalSize * RCL_UNITS_PER_SQUARE + RCL_UNITS_PER_SQUARE / 2) / +      ((RCL_HORIZONTAL_FOV_TAN * 2 * scaledSize) / RCL_UNITS_PER_SQUARE) +    : RCL_INFINITY; +} + +RCL_Unit RCL_castRay3D( +  RCL_Vector2D pos1, RCL_Unit height1, RCL_Vector2D pos2, RCL_Unit height2, +  RCL_ArrayFunction floorHeightFunc, RCL_ArrayFunction ceilingHeightFunc, +  RCL_RayConstraints constraints) +{ +  RCL_HitResult hits[constraints.maxHits]; +  uint16_t numHits; + +  RCL_Ray ray; + +  ray.start = pos1; + +  RCL_Unit distance; + +  ray.direction.x = pos2.x - pos1.x; +  ray.direction.y = pos2.y - pos1.y; + +  distance = RCL_len(ray.direction); + +  ray.direction = RCL_normalize(ray.direction);  + +  RCL_Unit heightDiff = height2 - height1; + +  RCL_castRayMultiHit(ray,floorHeightFunc,0,hits,&numHits,constraints); + +  RCL_Unit result = RCL_UNITS_PER_SQUARE; + +  int16_t squareX = RCL_divRoundDown(pos1.x,RCL_UNITS_PER_SQUARE); +  int16_t squareY = RCL_divRoundDown(pos1.y,RCL_UNITS_PER_SQUARE); + +  RCL_Unit startHeight = floorHeightFunc(squareX,squareY); + +  #define checkHits(comp,res) \ +  { \ +    RCL_Unit currentHeight = startHeight; \ +    for (uint16_t i = 0; i < numHits; ++i) \ +    { \ +      if (hits[i].distance > distance) \ +        break;\ +      RCL_Unit h = hits[i].arrayValue; \ +      if ((currentHeight comp h ? currentHeight : h) \ +         comp (height1 + (hits[i].distance * heightDiff) / distance)) \ +      { \ +        res = (hits[i].distance * RCL_UNITS_PER_SQUARE) / distance; \ +        break; \ +      } \ +      currentHeight = h; \ +    } \ +  } + +  checkHits(>,result) + +  if (ceilingHeightFunc != 0) +  { +    RCL_Unit result2 = RCL_UNITS_PER_SQUARE; +   +    startHeight = ceilingHeightFunc(squareX,squareY); + +    RCL_castRayMultiHit(ray,ceilingHeightFunc,0,hits,&numHits,constraints); + +    checkHits(<,result2) + +    if (result2 < result) +      result = result2; +  } + +  #undef checkHits + +  return result; +} + +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; + +  if (movesInPlane || force) +  { +    int16_t xSquareNew, ySquareNew; + +    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; + +    RCL_Unit currCeilHeight = 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; + +      if (ceilingHeightFunc != 0) +        currCeilHeight = ceilingHeightFunc(xSquare,ySquare); +    } + +    // 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 || \ +          currCeilHeight - height < \ +            RCL_CAMERA_COLL_HEIGHT_BELOW + RCL_CAMERA_COLL_HEIGHT_ABOVE)\ +        dir##Collides = 1;\ +      else if (ceilingHeightFunc != 0)\ +      {\ +        RCL_Unit height2 = ceilingHeightFunc(s1,s2);\ +        if ((height2 < topLimit) || ((height2 - height) < \ +          (RCL_CAMERA_COLL_HEIGHT_ABOVE + RCL_CAMERA_COLL_HEIGHT_BELOW)))\ +          dir##Collides = 1;\ +      }\ +    }\ +    else\ +      dir##Collides = floorHeightFunc(s1,s2) > RCL_CAMERA_COLL_STEP_HEIGHT; + +    // check 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) + +    if (xCollides || yCollides) +    { +      if (movesInPlane) +      { +        #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;\ + +        collHandle(x) +        collHandle(y) +       +        #undef collHandle +      } +      else +      { +        /* Player collides without moving in the plane; this can happen e.g. on +           elevators due to vertical only movement. This code can get executed +           when force == 1. */ + +        RCL_Vector2D squarePos; +        RCL_Vector2D newPos; + +        squarePos.x = xSquare * RCL_UNITS_PER_SQUARE; +        squarePos.y = ySquare * RCL_UNITS_PER_SQUARE; + +        newPos.x = +          RCL_max(squarePos.x + RCL_CAMERA_COLL_RADIUS + 1, +            RCL_min(squarePos.x + RCL_UNITS_PER_SQUARE - RCL_CAMERA_COLL_RADIUS - 1, +              camera->position.x)); + +        newPos.y =  +          RCL_max(squarePos.y + RCL_CAMERA_COLL_RADIUS + 1, +            RCL_min(squarePos.y + RCL_UNITS_PER_SQUARE - RCL_CAMERA_COLL_RADIUS - 1, +              camera->position.y)); + +        cornerNew.x = corner.x + (newPos.x - camera->position.x); +        cornerNew.y = corner.y + (newPos.y - camera->position.y); +      } +    } +    else  +    { +      /* If no non-diagonal collision is detected, a diagonal/corner collision +         can still happen, check it here. */ + +      if (xSquare != xSquareNew && ySquare != ySquareNew) +      { +        int8_t xyCollides = 0; +        collCheck(xy,xSquareNew,ySquareNew) +         +        if (xyCollides) +        { +          // normally should slide, but let's KISS and simply stop any movement +          cornerNew = corner; +        } +      } +    } + +    #undef collCheck + +    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; +} +  | 
