iOS中WebGL纹理渲染存在缺陷

3

使用three.js库进行WebGL纹理渲染的简单测试:

// Canvas dimensions

canvasW = Math.floor(0.9*window.innerWidth);
canvasH = Math.floor(0.75*canvasW);
cAR = canvasW / canvasH;
canvasWrapper = document.getElementById('canvasWrapper');
canvasWrapper.style.width=canvasW+'px';
canvasWrapper.style.height=canvasH+'px';

// Renderer

renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
console.log("Renderer pixel ratio = "+window.devicePixelRatio);
renderer.setSize(canvasW, canvasH);
canvas = renderer.domElement;
canvasWrapper.appendChild(canvas);

// Set up camera

cameraDist = 24;
camera = new THREE.PerspectiveCamera(25, cAR, 0.01, 1000);
cameraAngle = 0;
camera.position.x = cameraDist*Math.sin(cameraAngle);
camera.position.y = 0.3*cameraDist;
camera.position.z = cameraDist*Math.cos(cameraAngle);
camera.lookAt(new THREE.Vector3(0,0,0));

// Set up scene, consisting of texture-tiled ground

scene = new THREE.Scene();
groundWidth = 1000;
groundMaterial = null;
groundGeom = new THREE.PlaneGeometry(groundWidth,groundWidth);
groundGeom.rotateX(-Math.PI/2);
groundMesh = new THREE.Mesh(groundGeom, groundMaterial || new THREE.MeshBasicMaterial());
scene.add(groundMesh);
//window.requestAnimationFrame(draw);

// Insert texture once it has loaded

function setGroundTexture(texture)
{
  groundTexture = texture;
  groundTexture.wrapS = THREE.RepeatWrapping;
  groundTexture.wrapT = THREE.RepeatWrapping;
  groundTexture.repeat.set(groundWidth, groundWidth);
  groundTexture.anisotropy = renderer.getMaxAnisotropy();
  console.log("Texture anisotropy = "+groundTexture.anisotropy);
  groundMaterial = new THREE.MeshBasicMaterial({map: groundTexture});
  if (groundMesh)
  {
    groundMesh.material = groundMaterial;
    window.requestAnimationFrame(draw);
  };
}

// Start texture loading

//new THREE.TextureLoader().load("Texture.png", setGroundTexture, function (xhr) {}, function (xhr) {});
setGroundTexture(makeTexture());

// Render a frame

function draw()
{
  renderer.render(scene, camera);
}

// -------

function makeTexture() {
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = 256;
  ctx.canvas.height = 256;
  ctx.fillStyle = "rgb(238, 238, 238)";
  ctx.fillRect(0, 0, 256, 256);
  ctx.fillStyle = "rgb(208, 208, 208)";
  ctx.fillRect(0, 0, 128, 128);
  ctx.fillRect(128, 128, 128, 128);
  for (var y = 0; y < 2; ++y) {
    for (var x = 0; x < 2; ++x) {
      ctx.save();
      ctx.translate(x * 128 + 64, y * 128 + 64);
      ctx.lineWidth = 3;
      ctx.beginPath();
      var radius = 50;
      ctx.moveTo(radius, 0);
      for (var i = 0; i <= 6; ++i) {
        var a = i / 3 * Math.PI;
        ctx.lineTo(Math.cos(a) * radius, Math.sin(a) * radius);
      }
      ctx.stroke();
      ctx.restore();
    }
  }
  var tex = new THREE.Texture(ctx.canvas);
  tex.needsUpdate = true;
  return tex;
}
canvas, #canvasWrapper {margin-left: auto; margin-right: auto;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r78/three.js"></script>
<div id="canvasWrapper"></div>

在我尝试的桌面浏览器上渲染效果完美,但在iPad上渲染时会出现严重模糊,如下面页面中显示的截图所示。

桌面

enter image description here

iPad

enter image description here

在这两种情况下,纹理的各向异性为16(渲染器支持的最大值)。用于纹理的图像尺寸为256×256(必须为2的幂,以便进行重复纹理),将其放大或缩小无法解决问题。

纹理:

enter image description here

我将渲染器的像素比设置为匹配浏览器窗口大小,这意味着对于桌面系统,它为1,而对于iPad的Retina显示屏,它为2。这种方法通常可以在渲染的其他方面获得最佳结果,在任何情况下,在iPad上将像素比设置为1而不是2并不能改善纹理的外观。

因此,我的问题是:这是iOS WebGL中的一个错误,我只能忍受它,还是我可以在自己的代码中进行一些调整以在iOS设备上获得更好的结果?

编辑:这个three.js演示页面在iPad上的渲染效果也比桌面浏览器要差得多,并且演示的源代码使用了与我的代码相同的一般方法,这表明我可能错过了某些技巧,而这些技巧并不简单和显而易见。


我无法重现你的问题。iOS 的版本是什么?使用的是哪款 iPad?在 iOS 设置中是否开启了任何放大设置? - gman
@gman 对我来说,问题在iOS 9和iOS 10下都是一样的。我使用的是带视网膜显示屏的iPad 4(2048×1536)。在任何iOS设置中都没有放大(我猜你指的是辅助功能缩放?)我用脚本生成的纹理运行你的代码时也会出现同样的伪影。 - Greg Egan
@gman 这是我在iPad 4上运行的three.js演示的屏幕截图:http://gregegan.customer.netspace.net.au/tmp/iPadDemo.png,可以看到木板之间的黑线呈现出奇怪的锯齿状。但是,同样在iPad上运行这个WebGL演示时,我并没有遇到任何问题:http://media.tojicode.com/webgl-samples/anisotropy.html。有其他使用iPad 4的人看到这条消息吗...? - Greg Egan
2个回答

3
我无法完全解释问题的根源,但我找到了一个解决方法,表明问题的原因可能是数值精度的某种退化,我猜测是在GPU中发生的,并不会影响每个iPad图形卡。
这个解决方法涉及将地面的平面几何分割成多个正方形的网格,而最初只是一个单一的正方形(three.js可能将其分成2个三角形)。这可能会改变对象上的(u,v)坐标和纹理坐标与GPU中浮点精度极限相遇的方式。此外,将地面的大小从1000减小到200有所帮助。
讨厌的是,所有这些额外面的平面几何开销,即使它们在指定形状方面是完全冗余的。
在任何情况下,结果在我的桌面浏览器上看起来完全相同,但在我的iPad 4上看起来更好。
编辑:经过更仔细的测试,我认为细分THREE.PlaneGeometry没有任何区别,只有减少平铺平面的总体大小才有所帮助。事实上,通过使平铺平面的尺寸足够大,当尺寸为1000时在iPad 4上达到的任何限制也可以在我的iMac上达到80,000,正如代码片段的第二个版本所示。 (纹理在50,000左右开始退化,但80,000使畸变不可避免。)显然,没有真正的应用程序需要使用50,000 x 50,000个纹理副本平铺表面,但是每个方向几百个,这就是iPad 4开始出现问题的地方,并不奢侈。
代码片段的第一个版本,在iPad 4上解决了问题:

// Canvas dimensions

canvasW = Math.floor(0.9*window.innerWidth);
canvasH = Math.floor(0.75*canvasW);
cAR = canvasW / canvasH;
canvasWrapper = document.getElementById('canvasWrapper');
canvasWrapper.style.width=canvasW+'px';
canvasWrapper.style.height=canvasH+'px';

// Renderer

renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
console.log("Renderer pixel ratio = "+window.devicePixelRatio);
renderer.setSize(canvasW, canvasH);
canvas = renderer.domElement;
canvasWrapper.appendChild(canvas);

// Set up camera

cameraDist = 24;
camera = new THREE.PerspectiveCamera(25, cAR, 0.01, 1000);
cameraAngle = 0;
camera.position.x = cameraDist*Math.sin(cameraAngle);
camera.position.y = 0.3*cameraDist;
camera.position.z = cameraDist*Math.cos(cameraAngle);
camera.lookAt(new THREE.Vector3(0,0,0));

// Set up scene, consisting of texture-tiled ground

scene = new THREE.Scene();
// groundWidth = 1000;
// Reduce overall size of ground
groundWidth = 200;
groundMaterial = null;
// groundGeom = new THREE.PlaneGeometry(groundWidth,groundWidth);
// Split plane geometry into a grid of smaller squares
groundGeom = new THREE.PlaneGeometry(groundWidth,groundWidth,20,20);
groundGeom.rotateX(-Math.PI/2);
groundMesh = new THREE.Mesh(groundGeom, groundMaterial || new THREE.MeshBasicMaterial());
scene.add(groundMesh);
//window.requestAnimationFrame(draw);

// Insert texture once it has loaded

function setGroundTexture(texture)
{
  groundTexture = texture;
  groundTexture.wrapS = THREE.RepeatWrapping;
  groundTexture.wrapT = THREE.RepeatWrapping;
  groundTexture.repeat.set(groundWidth, groundWidth);
  groundTexture.anisotropy = renderer.getMaxAnisotropy();
  console.log("Texture anisotropy = "+groundTexture.anisotropy);
  groundMaterial = new THREE.MeshBasicMaterial({map: groundTexture});
  if (groundMesh)
  {
    groundMesh.material = groundMaterial;
    window.requestAnimationFrame(draw);
  };
}

// Start texture loading

//new THREE.TextureLoader().load("Texture.png", setGroundTexture, function (xhr) {}, function (xhr) {});
setGroundTexture(makeTexture());

// Render a frame

function draw()
{
  renderer.render(scene, camera);
}

// -------

function makeTexture() {
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = 256;
  ctx.canvas.height = 256;
  ctx.fillStyle = "rgb(238, 238, 238)";
  ctx.fillRect(0, 0, 256, 256);
  ctx.fillStyle = "rgb(208, 208, 208)";
  ctx.fillRect(0, 0, 128, 128);
  ctx.fillRect(128, 128, 128, 128);
  for (var y = 0; y < 2; ++y) {
    for (var x = 0; x < 2; ++x) {
      ctx.save();
      ctx.translate(x * 128 + 64, y * 128 + 64);
      ctx.lineWidth = 3;
      ctx.beginPath();
      var radius = 50;
      ctx.moveTo(radius, 0);
      for (var i = 0; i <= 6; ++i) {
        var a = i / 3 * Math.PI;
        ctx.lineTo(Math.cos(a) * radius, Math.sin(a) * radius);
      }
      ctx.stroke();
      ctx.restore();
    }
  }
  var tex = new THREE.Texture(ctx.canvas);
  tex.needsUpdate = true;
  return tex;
}
canvas, #canvasWrapper {margin-left: auto; margin-right: auto;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r78/three.js"></script>
<div id="canvasWrapper"></div>

代码片段的第二个版本,在2007年的iMac上会破坏纹理:

// Canvas dimensions

canvasW = Math.floor(0.9*window.innerWidth);
canvasH = Math.floor(0.75*canvasW);
cAR = canvasW / canvasH;
canvasWrapper = document.getElementById('canvasWrapper');
canvasWrapper.style.width=canvasW+'px';
canvasWrapper.style.height=canvasH+'px';

// Renderer

renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
console.log("Renderer pixel ratio = "+window.devicePixelRatio);
renderer.setSize(canvasW, canvasH);
canvas = renderer.domElement;
canvasWrapper.appendChild(canvas);

// Set up camera

cameraDist = 24;
camera = new THREE.PerspectiveCamera(25, cAR, 0.01, 1000);
cameraAngle = 0;
camera.position.x = cameraDist*Math.sin(cameraAngle);
camera.position.y = 0.3*cameraDist;
camera.position.z = cameraDist*Math.cos(cameraAngle);
camera.lookAt(new THREE.Vector3(0,0,0));

// Set up scene, consisting of texture-tiled ground

scene = new THREE.Scene();
// groundWidth = 1000;
// Increase the size of the plane to trigger the problem
groundWidth = 80000;
groundMaterial = null;
groundGeom = new THREE.PlaneGeometry(groundWidth,groundWidth);
groundGeom.rotateX(-Math.PI/2);
groundMesh = new THREE.Mesh(groundGeom, groundMaterial || new THREE.MeshBasicMaterial());
scene.add(groundMesh);
//window.requestAnimationFrame(draw);

// Insert texture once it has loaded

function setGroundTexture(texture)
{
  groundTexture = texture;
  groundTexture.wrapS = THREE.RepeatWrapping;
  groundTexture.wrapT = THREE.RepeatWrapping;
  groundTexture.repeat.set(groundWidth, groundWidth);
  groundTexture.anisotropy = renderer.getMaxAnisotropy();
  console.log("Texture anisotropy = "+groundTexture.anisotropy);
  groundMaterial = new THREE.MeshBasicMaterial({map: groundTexture});
  if (groundMesh)
  {
    groundMesh.material = groundMaterial;
    window.requestAnimationFrame(draw);
  };
}

// Start texture loading

//new THREE.TextureLoader().load("Texture.png", setGroundTexture, function (xhr) {}, function (xhr) {});
setGroundTexture(makeTexture());

// Render a frame

function draw()
{
  renderer.render(scene, camera);
}

// -------

function makeTexture() {
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = 256;
  ctx.canvas.height = 256;
  ctx.fillStyle = "rgb(238, 238, 238)";
  ctx.fillRect(0, 0, 256, 256);
  ctx.fillStyle = "rgb(208, 208, 208)";
  ctx.fillRect(0, 0, 128, 128);
  ctx.fillRect(128, 128, 128, 128);
  for (var y = 0; y < 2; ++y) {
    for (var x = 0; x < 2; ++x) {
      ctx.save();
      ctx.translate(x * 128 + 64, y * 128 + 64);
      ctx.lineWidth = 3;
      ctx.beginPath();
      var radius = 50;
      ctx.moveTo(radius, 0);
      for (var i = 0; i <= 6; ++i) {
        var a = i / 3 * Math.PI;
        ctx.lineTo(Math.cos(a) * radius, Math.sin(a) * radius);
      }
      ctx.stroke();
      ctx.restore();
    }
  }
  var tex = new THREE.Texture(ctx.canvas);
  tex.needsUpdate = true;
  return tex;
}
canvas, #canvasWrapper {margin-left: auto; margin-right: auto;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r78/three.js"></script>
<div id="canvasWrapper"></div>


1
格雷格·伊根的观察非常有道理。如果你不仅将平面细分,而且将UV坐标瓦片化,使其重复使用,而不是使用可能会修复它的大数值。

// Canvas dimensions

canvasW = Math.floor(0.9*window.innerWidth);
canvasH = Math.floor(0.75*canvasW);
cAR = canvasW / canvasH;
canvasWrapper = document.getElementById('canvasWrapper');
canvasWrapper.style.width=canvasW+'px';
canvasWrapper.style.height=canvasH+'px';

// Renderer

renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
console.log("Renderer pixel ratio = "+window.devicePixelRatio);
renderer.setSize(canvasW, canvasH);
canvas = renderer.domElement;
canvasWrapper.appendChild(canvas);

// Set up camera

cameraDist = 24;
camera = new THREE.PerspectiveCamera(25, cAR, 0.01, 1000);
cameraAngle = 0;
camera.position.x = cameraDist*Math.sin(cameraAngle);
camera.position.y = 0.3*cameraDist;
camera.position.z = cameraDist*Math.cos(cameraAngle);
camera.lookAt(new THREE.Vector3(0,0,0));

// Set up scene, consisting of texture-tiled ground

scene = new THREE.Scene();
// groundWidth = 1000;
// Reduce overall size of ground
groundWidth = 200;
groundMaterial = null;
// groundGeom = new THREE.PlaneGeometry(groundWidth,groundWidth);
// Split plane geometry into a grid of smaller squares
//groundGeom = new THREE.PlaneGeometry(groundWidth,groundWidth,20,20);
var groundGeom = new THREE.BufferGeometry();

var quads = groundWidth * groundWidth;
var positions = new Float32Array( quads * 6 * 3 );
var normals = new Float32Array( quads * 6 * 3 );
var texcoords = new Float32Array( quads * 6 * 2 );

for (var yy = 0; yy < groundWidth; ++yy) {
  for (var xx = 0; xx < groundWidth; ++xx) {
    var qoff = (yy * groundWidth + xx) * 6;
    var poff = qoff * 3;
    var x = xx - groundWidth / 2;
    var y = yy - groundWidth / 2;
    positions[poff +  0] = x;
    positions[poff +  1] = y;
    positions[poff +  2] = 0;
    
    positions[poff +  3] = x + 1;
    positions[poff +  4] = y;
    positions[poff +  5] = 0;
    
    positions[poff +  6] = x;
    positions[poff +  7] = y + 1;
    positions[poff +  8] = 0;

    positions[poff +  9] = x;
    positions[poff + 10] = y + 1;
    positions[poff + 11] = 0;
    
    positions[poff + 12] = x + 1;
    positions[poff + 13] = y;
    positions[poff + 14] = 0;
    
    positions[poff + 15] = x + 1;
    positions[poff + 16] = y + 1;
    positions[poff + 17] = 0;
    
    normals[poff +  0] = 0;
    normals[poff +  1] = 1;
    normals[poff +  2] = 0;
    
    normals[poff +  3] = 0;
    normals[poff +  4] = 1;
    normals[poff +  5] = 0;
    
    normals[poff +  6] = 0;
    normals[poff +  7] = 1;
    normals[poff +  8] = 0;

    normals[poff +  9] = 0;
    normals[poff + 10] = 1;
    normals[poff + 11] = 0;
    
    normals[poff + 12] = 0;
    normals[poff + 13] = 1;
    normals[poff + 14] = 0;
    
    normals[poff + 15] = 0;
    normals[poff + 16] = 1;
    normals[poff + 17] = 0;
    
    var toff = qoff * 2;

    texcoords[toff +  0] = 0;
    texcoords[toff +  1] = 0;
    
    texcoords[toff +  2] = 1;
    texcoords[toff +  3] = 0;
    
    texcoords[toff +  4] = 0;
    texcoords[toff +  5] = 1;

    texcoords[toff +  6] = 0;
    texcoords[toff +  7] = 1;
    
    texcoords[toff +  8] = 1;
    texcoords[toff +  9] = 0;
    
    texcoords[toff + 10] = 1;
    texcoords[toff + 11] = 1;
  }
}

groundGeom.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
groundGeom.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
groundGeom.addAttribute( 'uv', new THREE.BufferAttribute( texcoords, 2 ) );

groundGeom.computeBoundingSphere();

groundGeom.rotateX(-Math.PI/2);
groundMesh = new THREE.Mesh(groundGeom, groundMaterial || new THREE.MeshBasicMaterial());
scene.add(groundMesh);
//window.requestAnimationFrame(draw);

// Insert texture once it has loaded

function setGroundTexture(texture)
{
  groundTexture = texture;
  groundTexture.wrapS = THREE.RepeatWrapping;
  groundTexture.wrapT = THREE.RepeatWrapping;
  groundTexture.repeat.set(1, 1);
  groundTexture.anisotropy = renderer.getMaxAnisotropy();
  console.log("Texture anisotropy = "+groundTexture.anisotropy);
  groundMaterial = new THREE.MeshBasicMaterial({map: groundTexture});
  if (groundMesh)
  {
    groundMesh.material = groundMaterial;
    window.requestAnimationFrame(draw);
  };
}

// Start texture loading

//new THREE.TextureLoader().load("Texture.png", setGroundTexture, function (xhr) {}, function (xhr) {});
setGroundTexture(makeTexture());

// Render a frame

function draw()
{
  renderer.render(scene, camera);
}

// -------

function makeTexture() {
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = 256;
  ctx.canvas.height = 256;
  ctx.fillStyle = "rgb(238, 238, 238)";
  ctx.fillRect(0, 0, 256, 256);
  ctx.fillStyle = "rgb(208, 208, 208)";
  ctx.fillRect(0, 0, 128, 128);
  ctx.fillRect(128, 128, 128, 128);
  for (var y = 0; y < 2; ++y) {
    for (var x = 0; x < 2; ++x) {
      ctx.save();
      ctx.translate(x * 128 + 64, y * 128 + 64);
      ctx.lineWidth = 3;
      ctx.beginPath();
      var radius = 50;
      ctx.moveTo(radius, 0);
      for (var i = 0; i <= 6; ++i) {
        var a = i / 3 * Math.PI;
        ctx.lineTo(Math.cos(a) * radius, Math.sin(a) * radius);
      }
      ctx.stroke();
      ctx.restore();
    }
  }
  var tex = new THREE.Texture(ctx.canvas);
  tex.needsUpdate = true;
  return tex;
}
canvas, #canvasWrapper {margin-left: auto; margin-right: auto;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r78/three.js"></script>
<div id="canvasWrapper"></div>


太好了!这个版本在iPad 4上运行得非常流畅,虽然将大小从1000缩小到200只是一个很大的改进,但这一额外步骤进一步提高了质量。 - Greg Egan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接