Search code examples
c#xnaraycastingterrainheightmap

Basic (fake) raycasting on a 2D heightmap


I'm trying to shade a 2D heightmap using basic raycasting that checks if the ray is intercepted before it should shade it. However it's not working.

Map : Map

And the raycasting is giving me this:

  • Red is ray interception, but before intended position (so shading),
  • blue is ray interception in the correct place (so highlights or just as-is), and
  • yellow means no ray interaction before the while loop cut-out.

Badx2

The result should be red on back-facing slopes and areas behind large mountains (shadows) and blue on sun-facing slopes (highlights). There should not be any yellow. So this image indicates that either all rays are hitting the wrong place, or are intersected always somewhere else, which is impossible. I suspect the problem is with my trig.

Ray class:

class Ray
    {
        public Vector2 Position;
        public Vector2 Direction; // Think in XZ coordinates for these (they are on a perpendicular plane to the heightmap)
        // Angle is angle from horizon (I think), and height is height above zero (arbitrary)
        public float Angle, Height;
        private TerrainUnit[,] Terrainmap;
        private float U, V;

        public Ray(ref TerrainUnit[,] Terrainmap, float height, float angle)
        {
            this.Terrainmap = Terrainmap;
            this.Angle = angle;
            this.Height = this.V = height;
            
            // Create new straight vector
            this.Direction = new Vector2(0, 1);
            // Rotate it to the values determined by the angle
            this.Direction = Vector2.Transform(Direction, Matrix.CreateRotationX(Angle));
            //this.Direction = new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle));
            // Find the horizontal distance of the origin-destination triangle
            this.U = V / (float)Math.Tan(Angle);
            // Bleh just initialize the vector to something
            this.Position = new Vector2(U, V);
        }

        public void CastTo(int x, int y)
        {
            // Get the height of the target terrain unit
            float H = (float)Terrainmap[x, y].Height;
            // Find where the ray would have to be to intersect that terrain unit based on its angle and height
            Position = new Vector2(x - U, H + V);

            float Z = 1000 * (float)Terrainmap[0, y].Height;

            // As long as the ray is not below the terrain and not past the destination point
            while (Position.Y > Z && Position.X <= x)
            {
                // If the ray has passed into terrain bounds update Z every step
                if (Position.X > 0) Z = 1000 * (float)Terrainmap[(int)Position.X, y].Height;
                Position.X += Direction.X;
                Position.Y += Direction.Y;
            }

            Terrainmap[x, y].TypeColor = Color.Yellow;
            if ((int)Position.X == x) Terrainmap[x, y].TypeColor = Color.Blue;
            else Terrainmap[x, y].TypeColor = Color.Red;
        }
    }

Function that is casting each ray and how I am calling that:

if (lighting) CastSunRays(1f, MathHelper.PiOver4);

private void CastSunRays(float height, float angle)
{
    Ray ray = new Ray(ref Terrainmap, height, angle);

    for (int x = 0; x < Width; x++)
        for (int y = 0; y < Height; y++)
            ray.CastTo(x, y);
}

Solution

  • I ended up using a much simpler approach with Bresenham's Line Algorithm to find the intercept point; I imagine it's much faster and more efficient than the way I was trying to do it would have been.