How can you write an algorithm to properly fill a circle using lines from the center?

拜拜、爱过 提交于 2020-12-03 08:45:55

问题


Currently I try to write code for calculating the parts of the screen you can see and those who can't because of objects that block light in 2d, like in Among Us:

The code should run on a processor with very low specs (at least in 2020), the C64. On such a simple CPU it's not possible to do such complex math fast enough for a game, so I came up with an idea: First of all, I make everything tile based, that makes processing easier and also means that I can just change entire characters or their color cells. Then I just write code for the PC in Processing (that's a coding language similar to Java but easier to use) to calculate how rays of light would move (the following graphic should make that more understandable), first just with a rectangle (and a single quadrant):

Then I wrote some completely messy assembler code for using the recorded coordinates to just keep filling the tiles with an inverted character based on the number of the ray currently being drawn on the ray until they hit an object (/ the tile it wants to fill is not inverted and not a space) and then just go to the next ray. I reduced the radius to 7 so it just takes up 256 bytes, useful for ASM. And that totally worked, I was able to fix every single bug and the result was quite impressive, since I needed to add pause statements or everything ran so fast that you couldn't see anything.

After that worked, I tried it with a circle, setting the points using this code:

int pointNum = ceil(radius * PI * 2); // calculates the circumference
for(int i = 0;i < pointNum;i++){
  float angle = map(i, 0, pointNum, 0, PI*2);
  setPixel(sin(angle) * radius, cos(angle) * radius);
}

I previously used the Bresenham circle algorithm but that didn't quite work so I tried a more simple way. So ...

All the marked black tiles never get hit by any light, which is a pretty big issue, because it wouldn't make much sense in a game that you just can't see those tiles. The code I used, written in Processing, is:

float[] xPoints = new float[0];
float[] yPoints = new float[0];
float[] xPointsT;
float[] yPointsT;
float[] xPointsHad = new float[0];
float[] yPointsHad = new float[0];
int pos = 0;
float interpolPos = 0;

int radius = 12;
float tileSize = 800.0 / (2*radius+1);

String output = "  !byte ";
int pointNum = ceil(radius * PI * 2);
void setup() {
  size(800, 800);
  frameRate(60);
  xPointsT = new float[0];
  yPointsT = new float[0];
  /*for(int i = 0;i <= radius;i++){
    setPixel(radius, i);
    setPixel(i, radius);
  }*/ //Uncomment this and comment the next 4 lines to get the rectangle version
  for(int i = 0;i < pointNum;i++){
    float angle = map(i, 0, pointNum, 0, PI*2);
    setPixel(sin(angle) * radius, cos(angle) * radius);
  }
  xPoints = concat(xPoints, xPointsT);
  yPoints = concat(yPoints, yPointsT);
}
void draw(){
  if(interpolPos > radius){
    pos++;
    interpolPos = 0;
    println(output);
    output = "  !byte ";
  }
  float x=0, y=0;
  float interpolMul = interpolPos / radius;
  x = xPoints[pos] * interpolMul;
  y = yPoints[pos] * interpolMul;
  interpolPos+=1;//sorta the resolution
  background(0);
  
  stroke(255);
  for(int i = 0;i < 2*radius+1;i++){
    for(int j = 0;j < 2*radius+1;j++){
      if((round(x) + radius) == i && (round(y) + radius) == j){
        fill(0, 255, 0);
        if(output != "  !byte ")
          output += ", ";
        output += i-radius;
        output += ", ";
        output += j-radius;
        xPointsHad = append(xPointsHad, i);
        yPointsHad = append(yPointsHad, j);
      }
      else{
        int fillVal = 0;
        for(int k = 0; k < xPoints.length;k++){
          if(round(xPoints[k])+radius == i && round(yPoints[k])+radius == j){
            fillVal += 64;
          }
        }
        fill(0, 0, fillVal);
        if(fillVal == 0){
          for(int k = 0; k < xPointsHad.length;k++){
            if(round(xPointsHad[k]) == i && round(yPointsHad[k]) == j){
              fill(128, 0, 0);
            }
          }
        }
      }
      rect(i * tileSize, j * tileSize, tileSize, tileSize);
    }
  }
  
  strokeWeight(3);
  stroke(0, 255, 255, 64);
  for(int i = 0;i < xPoints.length;i++){
    line((float(radius)+0.5) * tileSize, (float(radius)+0.5) * tileSize, (float(radius)+0.5+xPoints[i]) * tileSize, (float(radius)+0.5+yPoints[i]) * tileSize);
  }
  strokeWeight(1);
  
  fill(255, 255, 0);
  ellipse((x + radius + 0.5) * tileSize, (y + radius + 0.5) * tileSize, 10, 10);
}

void setPixel(float _x, float _y){
  for(int i = 0; i < xPoints.length;i++){
    if(_x == xPoints[i] && _y == yPoints[i]){
      return;
    }
  }
  for(int i = 0; i < xPointsT.length;i++){
    if(_x == xPointsT[i] && _y == yPointsT[i]){
      return;
    }
  }
  
  xPointsT = append(xPointsT, _x);
  yPointsT = append(yPointsT, _y);
}

(Instructions to get the rectangle are in the code) Those mentioned tiles seem to be never hit because the rays on them just jump over them, but what can I do to prevent that? You can decrease interpolPos+=x; to hit more tiles because that way your steps are smaller, but that wastes quite some space, so I don't think that's a good solution. Ideally you could also just decrease the number of coordinates you draw to get a smaller vision. Has anyone a good idea how to do that?


回答1:


You have chosen wrong method to find all touched cells - instead of point-based way you need cell(squares)-based approach - ray intersects rectangle rather than point.

There is article of Amanatides and Woo "A Fast Voxel Traversal Algorithm for Ray Tracing" for 2D.

Practical implementation.

Example:

Quick-made tracing example. Rays emitted from left top corner go to blue points. If ray meets black cell obstacle, it stops. Pink cells are lighted by rays, grey ones are not.




回答2:


Okay, I found something that worked for me in my situation: I just used the part that totally works (the rectangle) and then just make that a circle by ignoring every tile hit that's further away from the light source then the radius + 0.5, because without + .5 the circle looks weird. You can try it yourself, here's the code:

float[] xPoints = new float[0];
float[] yPoints = new float[0];
float[] xPointsT;
float[] yPointsT;
float[] xPointsHad = new float[0];
float[] yPointsHad = new float[0];
int pos = 0;
float interpolPos = 0;

int radius = 7;
float tileSize = 800.0 / (2*radius+1);

int pointNum = ceil(radius * PI * 2);

String standardOutput = "  !align 15,0\n  !byte ";
void setup() {
  size(800, 800);
  frameRate(60);
  xPointsT = new float[0];
  yPointsT = new float[0];
  for(int i = 0;i <= radius;i++){
    setPixel(radius, i);
    setPixel(i, radius);
  } //Uncomment this and comment the next 4 lines to get the rectangle version
  /*for(int i = 0;i < pointNum;i++){
    float angle = map(i, 0, pointNum, 0, PI*2);
    setPixel(sin(angle) * radius, cos(angle) * radius);
  }*/
  xPoints = concat(xPoints, xPointsT);
  yPoints = concat(yPoints, yPointsT);
  
  xPointsT = new float[0];
  yPointsT = new float[0];
}
void draw(){
  if(interpolPos > radius){
    pos++;
    interpolPos = 0;
    String output = standardOutput;
    for(int i = 0;i < radius + 1;i++){
      int indexPos = floor(map(i, 0, radius + 1, 0, xPointsT.length));
      output += round(xPointsT[indexPos]);
      output += ",";
      output += round(yPointsT[indexPos]);
      if(i < radius){
        output += ", ";
      }
    }
    println(output);
    xPointsT = new float[0];
    yPointsT = new float[0];
  }
  float x=0, y=0;
  float interpolMul = interpolPos / radius;
  x = xPoints[pos] * interpolMul;
  y = yPoints[pos] * interpolMul;
  interpolPos+=1;//sorta the resolution
  background(0);
  
  stroke(255);
  for(int i = 0;i < 2*radius+1;i++){
    for(int j = 0;j < 2*radius+1;j++){
      if((round(x) + radius) == i && (round(y) + radius) == j && sqrt(sq(round(x)) + sq(round(y))) < radius + 0.5){
        fill(0, 255, 0);
        xPointsT = append(xPointsT, i-radius);
        yPointsT = append(yPointsT, j-radius);
        xPointsHad = append(xPointsHad, i);
        yPointsHad = append(yPointsHad, j);
      }
      else{
        int fillVal = 0;
        for(int k = 0; k < xPoints.length;k++){
          if(round(xPoints[k])+radius == i && round(yPoints[k])+radius == j){
            fillVal += 64;
          }
        }
        fill(0, 0, fillVal);
        if(fillVal == 0){
          for(int k = 0; k < xPointsHad.length;k++){
            if(round(xPointsHad[k]) == i && round(yPointsHad[k]) == j){
              fill(128, 0, 0);
            }
          }
        }
      }
      rect(i * tileSize, j * tileSize, tileSize, tileSize);
    }
  }
  
  strokeWeight(3);
  stroke(0, 255, 255, 64);
  for(int i = 0;i < xPoints.length;i++){
    line((float(radius)+0.5) * tileSize, (float(radius)+0.5) * tileSize, (float(radius)+0.5+xPoints[i]) * tileSize, (float(radius)+0.5+yPoints[i]) * tileSize);
  }
  strokeWeight(1);
  
  fill(255, 255, 0);
  ellipse((x + radius + 0.5) * tileSize, (y + radius + 0.5) * tileSize, 10, 10);
}

void setPixel(float _x, float _y){
  for(int i = 0; i < xPoints.length;i++){
    if(_x == xPoints[i] && _y == yPoints[i]){
      return;
    }
  }
  for(int i = 0; i < xPointsT.length;i++){
    if(_x == xPointsT[i] && _y == yPointsT[i]){
      return;
    }
  }
  
  xPointsT = append(xPointsT, _x);
  yPointsT = append(yPointsT, _y);
}

Besides the main difference to ignore tiles that are not in the circle, I also changed that I store the coordinates not in a String but in two arrays, because then I use code to stretch them when there are fewer then radius + 1 points, so I don't have to store multiple circles with different sizes in the C64's RAM, so it meets my main requirements: It should fill every tile and it should be downscalable by ignoring some points at the end of rays. And is if efficient? Uh ... there could be a better solution that fills the circle with fewer rays, but I don't care too much. Still, if you have an idea, it would be nice if you could tell me, but otherwise this question is solved.

Edit: I forgot to add a picture. Don't be confused, I modified the code after posting it so you can also see the blue tiles on the circle.



来源:https://stackoverflow.com/questions/65009185/how-can-you-write-an-algorithm-to-properly-fill-a-circle-using-lines-from-the-ce

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!