Search Unity

Raycasting engine experiment

Discussion in 'General Graphics' started by Deleted User, Apr 6, 2018.

  1. Deleted User

    Deleted User

    Guest

    Wasn't sure to put this in here, maybe the scripting section was more appropriate, but hwatever feel free to move it in case.

    So since I was bored and not in the mood to work on my game decided to try to make a working raycasting engine in unity. I mean the rendering technique used for games such as Wolfenstein 3D or Doom. Was following the very nice tutorial from Lode Vandevenne you can find here: http://lodev.org/cgtutor/raycasting.html

    More specifically the textured one. Porting the code to c# wasn't a problem, the approach is simple but the real problem is to output what is calculated there in Unity, and this is where I have a roadblock. FIrst thing I tried is to get the colors of pixels and use SetPixels to render a texture on a plane, you can imagine how bad this went. The whole thing dropped to 5fps, so researching a bit around foudn ways trough RenderTexture but honestly I'm a bit ignorant on the matter and couldn't find a way to do it.

    So my question is, there is any way in Unity I could output colors into a texture or render texture or whatever that doesn't use SetPixels? Maybe some shader magic which I know even less about.

    Some screenshots of current progress:

    screen0.png screen1.png

    Main.cs
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. namespace Raycaster
    5. {
    6.     public class Main : MonoBehaviour
    7.     {
    8.  
    9.         private bool isInitialized = false;                                        //Check if everything is initialized before goign with the loop
    10.  
    11.         public int screenWidth = 640, screenHeight = 480;                        //Real screen resolution
    12.         public int virtualWidth = 320, virtualHeight = 240;                        //Virtual resolution for raycast
    13.  
    14.         public int texWidth = 64;                                                //Max texture width
    15.         public int texHeight = 64;                                                //Max texture height
    16.  
    17.         private Texture[] texture;                                                //Texture set
    18.  
    19.         private Material material;                                                //Material for GL stuff
    20.         private Texture2D output;                                                //Final renderer texture
    21.         private Color32[] buffer;                                                //Buffer pixels for output texture
    22.  
    23.         private Map map;                                                        //Map stuff
    24.         private Player player;                                                    //Player positio nadn direction
    25.  
    26.         //Awake
    27.         void Awake ()
    28.         {
    29.             Init ();
    30.         }
    31.  
    32.         //Initialize
    33.         void Init ()
    34.         {
    35.             if (!isInitialized)
    36.             {
    37.                 if (!output)
    38.                 {
    39.                     output = new Texture2D (virtualWidth, virtualHeight);
    40.  
    41.                     if (buffer == null || buffer.Length <= 0)
    42.                         buffer = new Color32[virtualWidth * virtualHeight];
    43.  
    44.                     output.filterMode = FilterMode.Point;
    45.                     output.anisoLevel = 0;
    46.                 }
    47.  
    48.                 if (map == null)
    49.                     map = new Map ();
    50.  
    51.                 if (player == null)
    52.                     player = new Player (new Vector2(22.0f, 11.5f), new Vector2(-1.0f, 0.0f), new Vector2(0.0f, 0.66f));
    53.        
    54.                 #if !UNITY_EDITOR
    55.                     Screen.SetResolution (screenWidth, screenHeight, false);
    56.                 #endif
    57.  
    58.                 if (texture == null || texture.Length <= 0)
    59.                 {
    60.                     texture = new Texture[8];
    61.  
    62.                     for (int i = 0; i < texture.Length; i++)
    63.                         texture [i] = new Texture (i.ToString ());
    64.                 }
    65.  
    66.                 isInitialized = true;
    67.             }
    68.         }
    69.  
    70.         //Main loop stuff and things
    71.         void Update ()
    72.         {
    73.             if (isInitialized)
    74.             {
    75.                 for(int x = 0; x < virtualWidth; x++)
    76.                 {
    77.                     //calculate ray position and direction
    78.                     float cameraX = 2*x/(float)virtualWidth-1; //x-coordinate in camera space
    79.                     float rayDirX = player.direction.x + player.view.x*cameraX;
    80.                     float rayDirY = player.direction.y + player.view.y*cameraX;
    81.  
    82.                     //which box of the map we're in
    83.                     int mapX = (int)player.position.x;
    84.                     int mapY = (int)player.position.y;
    85.  
    86.                     //length of ray from current position to next x or y-side
    87.                     float sideDistX;
    88.                     float sideDistY;
    89.  
    90.                     //length of ray from one x or y-side to next x or y-side
    91.                     float deltaDistX = Mathf.Abs(1 / rayDirX);
    92.                     float deltaDistY = Mathf.Abs(1 / rayDirY);
    93.                     float perpWallDist;
    94.  
    95.                     //what direction to step in x or y-direction (either +1 or -1)
    96.                     int stepX;
    97.                     int stepY;
    98.  
    99.                     bool hit = false; //was there a wall hit?
    100.                     bool side = false; //was a NS or a EW wall hit?
    101.  
    102.                     //calculate step and initial sideDist
    103.                     if (rayDirX < 0)
    104.                     {
    105.                         stepX = -1;
    106.                         sideDistX = (player.position.x - mapX) * deltaDistX;
    107.                     }
    108.                     else
    109.                     {
    110.                         stepX = 1;
    111.                         sideDistX = (mapX + 1.0f - player.position.x) * deltaDistX;
    112.                     }
    113.  
    114.                     if (rayDirY < 0)
    115.                     {
    116.                         stepY = -1;
    117.                         sideDistY = (player.position.y - mapY) * deltaDistY;
    118.                     }
    119.                     else
    120.                     {
    121.                         stepY = 1;
    122.                         sideDistY = (mapY + 1.0f - player.position.y) * deltaDistY;
    123.                     }
    124.  
    125.                     //perform DDA
    126.                     while (!hit)
    127.                     {
    128.                         //jump to next map square, OR in x-direction, OR in y-direction
    129.                         if (sideDistX < sideDistY)
    130.                         {
    131.                             sideDistX += deltaDistX;
    132.                             mapX += stepX;
    133.                             side = false;
    134.                         }
    135.                         else
    136.                         {
    137.                             sideDistY += deltaDistY;
    138.                             mapY += stepY;
    139.                             side = true;
    140.                         }
    141.  
    142.                         //Check if ray has hit a wall
    143.                         if (map.tiles[mapX,mapY] > 0)
    144.                             hit = true;
    145.                     }
    146.  
    147.                     //Calculate distance of perpendicular ray (Euclidean distance will give fisheye effect!)
    148.                     if (!side)
    149.                         perpWallDist = (mapX - player.position.x + (1 - stepX) / 2) / rayDirX;
    150.                     else      
    151.                         perpWallDist = (mapY - player.position.y + (1 - stepY) / 2) / rayDirY;
    152.  
    153.                     //Calculate height of line to draw on screen
    154.                     int lineHeight = (int)(virtualHeight / perpWallDist);
    155.  
    156.                     //calculate lowest and highest pixel to fill in current stripe
    157.                     int drawStart = -lineHeight / 2 + virtualHeight / 2;
    158.                     if(drawStart < 0)
    159.                         drawStart = 0;
    160.                
    161.                     int drawEnd = lineHeight / 2 + virtualHeight / 2;
    162.                     if(drawEnd >= virtualHeight)
    163.                         drawEnd = virtualHeight - 1;
    164.  
    165.                     //texturing calculations
    166.                     int texNum = map.tiles[mapX,mapY] - 1; //1 subtracted from it so that texture 0 can be used!
    167.  
    168.                     //calculate value of wallX
    169.                     float wallX; //where exactly the wall was hit
    170.                     if (!side)
    171.                         wallX = player.position.y + perpWallDist * rayDirY;
    172.                     else      
    173.                         wallX = player.position.x + perpWallDist * rayDirX;
    174.                
    175.                     wallX -= Mathf.Floor((wallX));
    176.  
    177.                     //x coordinate on the texture
    178.                     int texX = (int)(wallX * (float)texWidth);
    179.                     if(!side && rayDirX > 0)
    180.                         texX = texWidth - texX - 1;
    181.                
    182.                     if(side && rayDirY < 0)
    183.                         texX = texWidth - texX - 1;
    184.  
    185.                     for(int y = drawStart; y < drawEnd; y++)
    186.                     {
    187.                         int d = y * 256 - virtualHeight * 128 + lineHeight * 128;  //256 and 128 factors to avoid floats
    188.  
    189.                         // TODO: avoid the division to speed this up
    190.                         int texY = ((d * texHeight) / lineHeight) / 256;
    191.                         Color32 color = texture[texNum].pixels[texHeight * texY + texX];
    192.  
    193.                         /*
    194.                         //make color darker for y-sides: R, G and B byte each divided through two with a "shift" and an "and"
    195.                         if(side)
    196.                             color = new Color32(color.r, color.g, color.b, color.a);
    197.                         */
    198.  
    199.                         buffer [x + y * virtualWidth] = color;
    200.                     }
    201.  
    202.                     #region MOVEMENTS
    203.  
    204.                     //speed modifiers
    205.                     float moveSpeed = Time.deltaTime * 0.01f; //the constant value is in squares/second
    206.                     float rotSpeed = Time.deltaTime * 0.01f; //the constant value is in radians/second
    207.  
    208.                     //move forward if no wall in front of you
    209.                     if (Input.GetKey(KeyCode.W))
    210.                     {
    211.                         if(map.tiles[(int)(player.position.x + player.direction.x * moveSpeed),(int)player.position.y] == 0)
    212.                             player.position.x += player.direction.x * moveSpeed;
    213.                         if(map.tiles[(int)player.position.x,(int)(player.position.y + player.direction.y * moveSpeed)] == 0)
    214.                             player.position.y += player.direction.y * moveSpeed;
    215.                     }
    216.                     //move backwards if no wall behind you
    217.                     if (Input.GetKey(KeyCode.S))
    218.                     {
    219.                         if(map.tiles[(int)(player.position.x - player.direction.x * moveSpeed),(int)player.position.y] == 0)
    220.                             player.position.x -= player.direction.x * moveSpeed;
    221.                         if(map.tiles[(int)player.position.x,(int)(player.position.y - player.direction.y * moveSpeed)] == 0)
    222.                             player.position.y -= player.direction.y * moveSpeed;
    223.                     }
    224.                     //rotate to the right
    225.                     if (Input.GetKey(KeyCode.D))
    226.                     {
    227.                         //both camera direction and camera plane must be rotated
    228.                         float oldDirX = player.direction.x;
    229.                         player.direction.x = player.direction.x * Mathf.Cos(-rotSpeed) - player.direction.y * Mathf.Sin(-rotSpeed);
    230.                         player.direction.y = oldDirX * Mathf.Sin(-rotSpeed) + player.direction.y * Mathf.Cos(-rotSpeed);
    231.                         float oldPlaneX = player.view.x;
    232.                         player.view.x = player.view.x * Mathf.Cos(-rotSpeed) - player.view.y * Mathf.Sin(-rotSpeed);
    233.                         player.view.y = oldPlaneX * Mathf.Sin(-rotSpeed) + player.view.y * Mathf.Cos(-rotSpeed);
    234.                     }
    235.                     //rotate to the left
    236.                     if (Input.GetKey(KeyCode.A))
    237.                     {
    238.                         //both camera direction and camera plane must be rotated
    239.                         float oldDirX = player.direction.x;
    240.                         player.direction.x = player.direction.x * Mathf.Cos(rotSpeed) - player.direction.y * Mathf.Sin(rotSpeed);
    241.                         player.direction.y = oldDirX * Mathf.Sin(rotSpeed) + player.direction.y * Mathf.Cos(rotSpeed);
    242.                         float oldPlaneX = player.view.x;
    243.                         player.view.x = player.view.x * Mathf.Cos(rotSpeed) - player.view.y * Mathf.Sin(rotSpeed);
    244.                         player.view.y = oldPlaneX * Mathf.Sin(rotSpeed) + player.view.y * Mathf.Cos(rotSpeed);
    245.                     }
    246.                 }
    247.  
    248.                 #endregion
    249.  
    250.                 output.SetPixels32 (buffer);
    251.                 output.Apply ();
    252.             }
    253.         }
    254.  
    255.         void OnPostRender ()
    256.         {
    257.             if (!material)
    258.             {
    259.                 material = new Material (Shader.Find ("Unlit/Texture"));
    260.                 material.mainTexture = output;
    261.             }
    262.  
    263.             GL.PushMatrix();
    264.             material.SetPass(0);
    265.             GL.LoadOrtho();
    266.             GL.Begin(GL.QUADS);
    267.             GL.TexCoord2(0, 0);
    268.             GL.Vertex3(0, 0, 0);
    269.             GL.TexCoord2(0, 1);
    270.             GL.Vertex3(0, 1, 0);
    271.             GL.TexCoord2(1, 1);
    272.             GL.Vertex3(1,1, 0);
    273.             GL.TexCoord2(1, 0);
    274.             GL.Vertex3(1, 0, 0);
    275.             GL.End();
    276.             GL.PopMatrix();
    277.         }
    278.     }
    279. }
    280.  
    Map.cs
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. namespace Raycaster
    5. {
    6.     public class Map
    7.     {
    8.         public int width = 24;
    9.         public int height = 24;
    10.  
    11.         public int[,] tiles = new int[,]
    12.         {
    13.             {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,7,7,7,7,7,7,7,7},
    14.             {4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,7},
    15.             {4,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7},
    16.             {4,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7},
    17.             {4,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,7},
    18.             {4,0,4,0,0,0,0,5,5,5,5,5,5,5,5,5,7,7,0,7,7,7,7,7},
    19.             {4,0,5,0,0,0,0,5,0,5,0,5,0,5,0,5,7,0,0,0,7,7,7,1},
    20.             {4,0,6,0,0,0,0,5,0,0,0,0,0,0,0,5,7,0,0,0,0,0,0,8},
    21.             {4,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,7,7,1},
    22.             {4,0,8,0,0,0,0,5,0,0,0,0,0,0,0,5,7,0,0,0,0,0,0,8},
    23.             {4,0,0,0,0,0,0,5,0,0,0,0,0,0,0,5,7,0,0,0,7,7,7,1},
    24.             {4,0,0,0,0,0,0,5,5,5,5,0,5,5,5,5,7,7,7,7,7,7,7,1},
    25.             {6,6,6,6,6,6,6,6,6,6,6,0,6,6,6,6,6,6,6,6,6,6,6,6},
    26.             {8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4},
    27.             {6,6,6,6,6,6,0,6,6,6,6,0,6,6,6,6,6,6,6,6,6,6,6,6},
    28.             {4,4,4,4,4,4,0,4,4,4,6,0,6,2,2,2,2,2,2,2,3,3,3,3},
    29.             {4,0,0,0,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,0,0,0,2},
    30.             {4,0,0,0,0,0,0,0,0,0,0,0,6,2,0,0,5,0,0,2,0,0,0,2},
    31.             {4,0,0,0,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,2,0,2,2},
    32.             {4,0,6,0,6,0,0,0,0,4,6,0,0,0,0,0,5,0,0,0,0,0,0,2},
    33.             {4,0,0,5,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,2,0,2,2},
    34.             {4,0,6,0,6,0,0,0,0,4,6,0,6,2,0,0,5,0,0,2,0,0,0,2},
    35.             {4,0,0,0,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,0,0,0,2},
    36.             {4,4,4,4,4,4,4,4,4,4,1,1,1,2,2,2,2,2,2,3,3,3,3,3}
    37.         };
    38.  
    39.         //Constructor
    40.         public Map ()
    41.         {
    42.  
    43.         }
    44.  
    45.         //Constructor args
    46.         public Map (string id)
    47.         {
    48.  
    49.         }
    50.     }
    51. }
    52.  
    53.  
    Player.cs
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. namespace Raycaster
    5. {
    6.     public class Player
    7.     {
    8.  
    9.         public Vector2 position;
    10.         public Vector2 direction;
    11.         public Vector2 view;
    12.  
    13.         //Constructor
    14.         public Player ()
    15.         {
    16.  
    17.         }
    18.  
    19.         //Constructor args
    20.         public Player (Vector2 pos, Vector2 dir, Vector2 plane)
    21.         {
    22.             this.position.x = pos.x;
    23.             this.position.y = pos.y;
    24.  
    25.             this.direction.x = dir.x;
    26.             this.direction.y = dir.y;
    27.  
    28.             this.view.x = plane.x;
    29.             this.view.y = plane.y;
    30.         }
    31.     }
    32. }
    33.  
    Texture.cs
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. namespace Raycaster
    5. {
    6.     public class Texture
    7.     {
    8.  
    9.         public int width;
    10.         public int height;
    11.  
    12.         public Color32[] pixels;
    13.  
    14.         //Constructor
    15.         public Texture ()
    16.         {
    17.  
    18.         }
    19.  
    20.         //Constructor args
    21.         public Texture (string id)
    22.         {
    23.             if (!string.IsNullOrEmpty (id))
    24.             {
    25.                 Texture2D tex = Resources.Load<Texture2D> (id) as Texture2D;
    26.  
    27.                 if(tex != null)
    28.                 {
    29.                     this.width = tex.width;
    30.                     this.height = tex.height;
    31.                     this.pixels = tex.GetPixels32 ();
    32.                 }
    33.             }
    34.         }
    35.     }
    36. }
    37.  
    38.  
    I know no one would want to waste time to such things, isn't for a real use but just proof of concept if it can be done so maybe there is someone out there that has time to waste too to help to find a way.

    Added a bit more refined and separate classes for thigns like player and map. Also performance increased by a lot, from the 40 fps I ahd at the beginning to an average of 800. Could still be more optimized on the lines calculation.

    Also there is pixel bleeding on ceilign and floor since there is nothing beign rendered there.
     
    Last edited by a moderator: Apr 7, 2018
  2. DerrickMoore

    DerrickMoore

    Joined:
    Feb 4, 2018
    Posts:
    246
    Gurney Halleck on mood


    I kid, joking.. if you do not take a break from a project every now and then you can "burn out"... I know so so many gamedevs who "burnt out" in their 30s..

    We used to call that kind of engine a "scan-line renderer".. my very first game was a Doom-clone that used "oooh ahh" "splines" as well as straight edges... our game editor looked just like the one pictured in the raycasting link you posted.

    as far as "put colours onto a texture".. yes, I beleave so, quite easily, if I understand... take the Maaterial your texture map is in, and set the Material's size to "2"... then in the 2nd slot that should appear drag your 2nd material onto it.. the second material can be set for transparent or fade, etc and could include only a solid color, a gradiant. or a whole new texturemap..
     
  3. Deleted User

    Deleted User

    Guest

    Lol yeah I'm trying to avoid a burnout, jsut want to relax a few days, even with something pointless as this experiment.

    By the way. the main problem is the Texture2D.Apply() performance, is no problem to render the thing to a quad, problem is to not make it crawl to the ground in performance. Managed to optimize it a bit better and now I get 40fps, but seems a bit low still especially for something that used to work on a 386.

    Updated Main.cs (added a PostRender with GL commands to draw the stuff and output texture), see first post.
     
    Last edited by a moderator: Apr 7, 2018
    DerrickMoore likes this.
  4. DerrickMoore

    DerrickMoore

    Joined:
    Feb 4, 2018
    Posts:
    246
    slow? only because you've written a game engine and a renderer within a gameengine/renderer.. I bet there are a thousand Unity bits and pieces that you could turn off and have it run faster..

    I'm trying to remember how lighting was done on our game.. super primitive, not even Unity's pseudo ray-tracing and shadows.. I'm pretty sure that our wall textures were a whopping 128x128 pixels (Doom had 64x64 walls, I think) and there were only 32 unique colours in our textures, only 256 colours in the whole game.

    Also, I noticed you are running it at VGA graphics.. Try EGA, 300x 200-something, I forget.. and I'm pretty sure that both us and Doom "doubled-up" the horizontal on-screen pixels, to decrease the amount of scanlines
     
    Deleted User likes this.
  5. Deleted User

    Deleted User

    Guest

    Yeah at 320x240 runs smooth, around 250fps, I think I can still optimize it more by getting rid of SetPixel and drop the whole set of pixels with SetPixels32, it should make the process faster in theory.
     
  6. Deleted User

    Deleted User

    Guest

    So SetPixels32 sure helped, increase by 15x times more in performance, even at 640x480 it behave pretty good. Added the new code on first post for whoever gives a damn.

    All that need to be done is have a camera in a scene and put the Main.cs script on it, the current map uses 8 textures, just put 8 textures with the name as index from 0 to 7 in Resources folder and they are auto loaded on startup (textures must be 64x64 pixels), nothign else needed.

    Added also a couple of screenshots on first post.
     
    Last edited by a moderator: Apr 7, 2018