Игра жизни Конвея в задачах 3D FPS

Я пытаюсь реализовать игру жизни Конвея в 3D. По сути, я экспериментирую с дополнительным измерением.

Я создаю список кубов в начале игры и даю каждому из них индекс, который будет связан с логическим объектом, где я вызываю twgl.drawObjectList, если он жив, иначе я пропущу его в функции, которую я Я использую requestAnimationFrame на.

Проблема в том, что FPS падает ниже 1, когда я делаю игру 50*50*50 (125000 кубов). Это нормально? Я делаю правильный подход?

Редактировать:

function newGame (xDimV, yDimV, zDimV, gameSelected = false) {
// No game to load
if (!gameSelected) {
    xDim = xDimV;
    yDim = yDimV;
    zDim = zDimV;
} else {
    xDim = gameSelected[0][0].length;
    yDim = gameSelected[0].length;
    zDim = gameSelected.length;
}
myGame = Object.create(game);
myGame.consutructor(xDim , yDim , zDim, gameSelected);
objects = [];
for (var z = 0; z < zDim; z++) {
    for (var y = 0; y < yDim; y++){
        for (var x = 0; x < xDim; x++){

            var uniforms = {
                u_colorMult: chroma.hsv(emod(baseHue + rand(0, 120), 360), rand(0.5,
                                    1), rand(0.5, 1)).gl(),
                u_world: m4.identity(),
                u_worldInverseTranspose: m4.identity(),
                u_worldViewProjection: m4.identity(),
            };

            var drawObjects = [];
            drawObjects.push({
                programInfo: programInfo,
                bufferInfo: cubeBufferInfo,
                uniforms: uniforms,
            });

            objects.push({
                translation: [(x*scale)-xDim*scale/2, (z*scale), (y*scale)-yDim*scale/2],
                scale: scale,
                uniforms: uniforms,
                bufferInfo: cubeBufferInfo,
                programInfo: programInfo,
                drawObject: drawObjects,
                index: [z, y, x],
            });
        }
    }
}
requestAnimationFrame(render);
}

var then = 0;
function render(time) {
time *= 0.001;
var elapsed = time - then;
then = time;

twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.clearColor(255, 255, 0, 0.1);
var fovy = 30 * Math.PI / 180;
var projection = m4.perspective(fovy, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.5, 10000);

var eye = [cameraX, cameraY, cameraZ];
var target = [cameraX, cameraY, 10];
var up = [0, 1, 0];

var camera = m4.lookAt(eye, target, up);
var view = m4.inverse(camera);
var viewProjection = m4.multiply(projection, view);
viewProjection =  m4.rotateX(viewProjection, phi);
viewProjection = m4.rotateY(viewProjection, theta);
targetTimer -= elapsed;

objects.forEach(function(obj) {
    var uni = obj.uniforms;
    var world = uni.u_world;
    m4.identity(world);
    m4.translate(world, obj.translation, world);
    m4.scale(world, [obj.scale, obj.scale, obj.scale], world);
    m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose);
    m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);

    if (myGame.life[obj.index[0]][obj.index[1]][obj.index[2]] === 1) {
        twgl.drawObjectList(gl, obj.drawObject);
    }
});
if (targetTimer <= 0 && !paused) {
    targetTimer = targetChangeInterval / speed;
    myGame.nextGen();
    setGameStatus();
    myGame.resetStatus();
}
requestAnimationFrame(render);
}

Заранее спасибо.

2 ответа

Решение

125 тыс. Кубов - это довольно много. Как правило, в типичных играх ААА от 1000 до 5000 розыгрышей. В сети есть сбои различных игровых движков, и сколько набираемых вызовов они генерируют.

Вот разговор с несколькими методами. Он включает в себя помещение всех кубов в одну гигантскую сетку и перемещение их в JavaScript, чтобы они были фактически одним вызовом отрисовки.

Если бы это был я, я бы сделал это и сделал бы текстуру с одним пикселем на куб. Таким образом, для кубов 125k эта текстура будет выглядеть как 356x356, хотя я, вероятно, выберу что-то более подходящее для размера куба, например, 500x300 (так как каждый срез лица равен 50x50) Для каждой вершины каждого куба у меня был бы атрибут с UV, указывающим на определенный пиксель в этой текстуре. Другими словами, для первых вершин первого куба будет атрибут UV, который повторяется 36 раз, в новом UV для 2-го куба, который повторяется 36 раз,

 attribute vec2 cubeUV;

Затем я могу использовать cubeUV для поиска пикселя в текстуре, должен ли куб быть включен или выключен

 attribute vec2 cubeUV;
 uniform sampler2D lifeTexture;     

 void main() {
   float cubeOn = texture2D(lifeTexture, cubeUV).r;
 }

Я мог бы довольно легко обрезать куб

   if (cubeOn < 0.5) {
     gl_Position = vec4(2, 2, 2, 1);  // outside clip space
     return;
   }

   // otherwise do the calcs for a cube

В этом случае кубы не нужно перемещать, поэтому все, что JavaScript должен делать, - это вычислять жизнь в некоторых кадрах. Uint8Array а затем позвоните

gl.bindTexture(gl.TEXTURE_2D, lifeTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0,
              gl.LUMINANCE, gl.UNSIGNED_BYTE, lifeStatusUint8Array);

каждый кадр и сделать один колл-розыгрыш.

Примечание: Вы можете эффективно видеть примеры этого типа шейдеров здесь, за исключением того, что этот шдаер не смотрит на текстуру с работающей в ней жизнью, а смотрит на текстуру с 4-секундными аудиоданными в ней. Это также генерирует cubeId от vertexId и генерация вершин куба и нормалей из vertexId, Это сделало бы это медленнее, чем помещение этих данных в атрибуты, но это пример позиционирования или рисования кубов на основе данных, поступающих из текстуры.

const vs = `
attribute vec4 position;
attribute vec3 normal;
attribute vec2 cubeUV;

uniform mat4 u_matrix;
uniform sampler2D u_lifeTex;

varying vec3 v_normal;

void main() {
  float on = texture2D(u_lifeTex, cubeUV).r;
  if (on < .5) {
     gl_Position = vec4(20, 20, 20, 1);
     return;
  }
  gl_Position = u_matrix * position;  
  v_normal = normal;
}
`;

const fs = `
precision mediump float;

varying vec3 v_normal;

void main() {
  gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
`;

const oneFace = [
  [ -1, -1, ],
  [  1, -1, ],
  [ -1,  1, ],
  [ -1,  1, ],
  [  1, -1, ],
  [  1,  1, ],
];

const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");

// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const cubeSize = 50;
const texBuf = makeCubeTexBuffer(gl, cubeSize);
const tex = twgl.createTexture(gl, {
  src: texBuf.buffer,
  width: texBuf.width,
  format: gl.LUMINANCE,
  wrap: gl.CLAMP_TO_EDGE,
  minMag: gl.NEAREST,
});

const arrays = makeCubes(cubeSize, texBuf);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each array
const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);

function render(time) {
  time *= 0.001; // seconds
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.enable(gl.DEPTH_TEST);
  //gl.enable(gl.CULL_FACE);
  
  const fov = Math.PI * .25;
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = .01;
  const zFar  = 1000;
  const projection = m4.perspective(fov, aspect, zNear, zFar);
  
  const radius = cubeSize * 2.5;
  const speed = time * .1;
  const position = [
     Math.sin(speed) * radius, 
     Math.sin(speed * .7) * radius * .7, 
     Math.cos(speed) * radius,
  ];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const camera = m4.lookAt(position, target, up);
  
  const view = m4.inverse(camera);
  
  const mat = m4.multiply(projection, view);

  // do life
  // (well, randomly turn on/off cubes)
  for (let i = 0; i < 100; ++i) {
     texBuf.buffer[Math.random() * texBuf.buffer.length | 0] = Math.random() > .5 ? 255 : 0;
  }
  
  gl.bindTexture(gl.TEXTURE_2D, tex);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, texBuf.width, texBuf.height,
                0, gl.LUMINANCE, gl.UNSIGNED_BYTE, texBuf.buffer);
  
  gl.useProgram(programInfo.program)

  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

  twgl.setUniforms(programInfo, {
    u_matrix: mat,
    u_lifeTex: tex,
  });

  // calls gl.drawArrays or gl.drawElements
  twgl.drawBufferInfo(gl, bufferInfo);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

// generate cubes
function makeCube(vertOffset, off, uv, arrays) {
  const positions = arrays.position;
  const normals = arrays.normal;
  const cubeUV = arrays.cubeUV;
  
  for (let f = 0; f < 6; ++f) {
    const axis = f / 2 | 0;    
    const sign = f % 2 ? -1 : 1;
    const major = (axis + 1) % 3;
    const minor = (axis + 2) % 3;

    for (let i = 0; i < 6; ++i) {
      const offset2 = vertOffset * 2;
      const offset3 = vertOffset * 3;
      positions[offset3 + axis ] = off[axis]  + sign;
      positions[offset3 + major] = off[major] + oneFace[i][0];
      positions[offset3 + minor] = off[minor] + oneFace[i][1];
      normals[offset3 + axis ] = sign;
      normals[offset3 + major] = 0;
      normals[offset3 + minor] = 0;
      
      cubeUV[offset2 + 0] = uv[0]; 
      cubeUV[offset2 + 1] = uv[1]; 
      ++vertOffset;
    }
  }
  return vertOffset;
}

function makeCubes(size, texBuf) {
  const numCubes = size * size * size;
  const numVertsPerCube = 36;
  const numVerts = numCubes * numVertsPerCube;
  const slicesAcross = texBuf.width / size | 0;
  const arrays = {
    position: new Float32Array(numVerts * 3),
    normal: new Float32Array(numVerts * 3),
    cubeUV: new Float32Array(numVerts * 2),
  };
  
  let spacing = size * 1.2;
  let vertOffset = 0;
  for (let z = 0; z < size; ++z) {
    const zoff = (z / (size - 1) * 2 - 1) * spacing;
    for (let y = 0; y < size; ++y) {
      const yoff = (y / (size - 1) * 2 - 1) * spacing;
      for (let x = 0; x < size; ++x) {
        const xoff = (x / (size - 1) * 2 - 1) * spacing;
        const sx = z % slicesAcross;
        const sy = z / slicesAcross | 0;
        const uv = [
          (sx * size + x + 0.5) / texBuf.width, 
          (sy * size + y + 0.5) / texBuf.height,
        ];
        vertOffset = makeCube(vertOffset, [xoff, yoff, zoff], uv, arrays);
      }
    }
  }
  arrays.cubeUV = {
    numComponents: 2,
    data: arrays.cubeUV,
  };
  return arrays;
}

function makeCubeTexBuffer(gl, cubeSize) {
  const numCubes = cubeSize * cubeSize * cubeSize;
  const maxTextureSize = Math.min(gl.getParameter(gl.MAX_TEXTURE_SIZE), 2048);
  const maxSlicesAcross = maxTextureSize / cubeSize | 0;
  const slicesAcross = Math.min(cubeSize, maxSlicesAcross);
  const slicesDown = Math.ceil(cubeSize / slicesAcross);
  const width = slicesAcross * cubeSize;
  const height = slicesDown * cubeSize;
  const buffer = new Uint8Array(width * height);
  return {
    buffer: buffer,
    slicesAcross: slicesAcross,
    slicesDown: slicesDown,
    width: width,
    height: height,
  };
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas></canvas> 

Как видно из приведенных ниже комментариев, использование большого объединенного меша оказывается в 1,3 раза быстрее, чем при использовании инстансированного рисования. Вот 3 образца

  1. большая сетка с использованием текстуры uvs (так же, как выше)
  2. создается с использованием текстуры uvs (меньше данных, тот же шейдер)
  3. не содержит текстуры (нет текстуры, данные о жизни находятся в буфере / атрибуте)

Для меня на моей машине #1 можно делать 60x60x60 кубов (216000) со скоростью 60 кадров в секунду, тогда как и № 2, и № 3 получают только 56x56x56 кубов (175616) со скоростью 60 кадров в секунду. Конечно, другие графические процессоры / системы / браузеры могут отличаться.

Снижение fps происходит от двух вещей, наиболее вероятно:

  1. Затраты на выполнение 125k матричных операций на каждом тике.
  2. Накладные расходы на выполнение 125 тыс. Раздач.

Вы можете посмотреть на создание экземпляров http://blog.tojicode.com/2013/07/webgl-instancing-with.html?m=1

И, возможно, переместить матрицу в шейдер

Другие вопросы по тегам