iOS WebGL纹理渲染存在缺陷

时间:2022-06-15 04:19:48

This simple test of WebGL texture rendering using the three.js library:

这是一个简单的WebGL纹理渲染测试。js库:

//	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>

renders perfectly on the desktop browsers I've tried, but is badly blurred when rendered on an iPad, as shown by the screenshot further down the page.

在我尝试过的桌面浏览器上,渲染效果非常完美,但在iPad上渲染时,就会变得非常模糊,正如页面下方的截图所示。

Desktop

桌面

iOS WebGL纹理渲染存在缺陷

iPad

iPad

iOS WebGL纹理渲染存在缺陷

In both cases, the texture is rendered with an anisotropy of 16 (the maximum supported by the renderer). The image used for the texture has dimensions 256 × 256 (a power of 2, which is necessary for repeated textures), and making it larger or smaller doesn't fix the problem.

在这两种情况下,纹理都以16(渲染器支持的最大)的各向异性来呈现。用于纹理的图像尺寸256×256(2的乘方,必要重复纹理),并让它更大或更小并没有解决这个问题。

texture:

材质:

iOS WebGL纹理渲染存在缺陷

I'm setting the renderer's pixel ratio to match the browser window, which means it is 1 for desktop systems and 2 for the iPad's retina display. This approach generally gives the best results for other aspects of rendering, and in any case setting the pixel ratio to 1 on the iPad, instead of 2, doesn't improve the appearance of the texture.

我正在设置渲染器的像素比以匹配浏览器窗口,这意味着桌面系统是1,iPad的视网膜显示屏是2。这种方法通常为渲染的其他方面提供最好的结果,无论如何,在iPad上将像素比设置为1而不是2,不会改善纹理的外观。

So my question is: is this a bug in iOS WebGL that I'll just have to live with, or is there something I can tweak in my own code to get better results on iOS devices?

我的问题是:这是ioswebgl中的一个bug吗?我只需要和它一起生活,或者我可以在自己的代码中做一些调整,以便在iOS设备上得到更好的结果?

Edit: This three.js demo page also renders much less clearly on the iPad than on desktop browsers, and the source for the demo uses the same general approach as my own code, which suggests that whatever trick I'm missing, it's not something simple and obvious.

编辑:这三个。js demo页面在iPad上的呈现也比在桌面浏览器上要清晰得多,demo的源代码使用了与我自己代码相同的通用方法,这表明无论我错过了什么技巧,它都不是简单而明显的东西。

2 个解决方案

#1


1  

Greg Egan's observation makes a lot of sense. If you not only subdivide the plane but tile the UV coords so they repeat instead of using large numbers that might fix it.

格雷格·伊根的观察很有道理。如果你不仅细分平面,而且平铺UV coords所以他们重复而不是使用大数来修正它。

//	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>

#2


1  

I can't fully explain the source of the problem, but I've found a work-around that suggests that the cause is some kind of degradation of numerical precision, I guess in the GPU, that doesn't occur with every iPad graphics card.

我不能完全解释问题的根源,但我找到了一个解决办法,它表明问题的原因是某种数值精度的下降,我猜在GPU中,并不是所有iPad显卡都会出现这种情况。

The work-around involves splitting the plane geometry for the ground, which was originally just a single square (which three.js presumably divides into 2 triangles), into a grid of multiple squares. Presumably this changes something in the way the (u,v) coordinates on the object and the texture coordinates run up against the limits of floating point precision in the GPU. Also, reducing the size of the ground from 1000 to 200 helps.

围绕这个问题的解决方法是将地面的平面几何分割开来,这原本只是一个正方形(三个正方形)。js大概分成两个三角形),组成一个由多个正方形组成的网格。大概这改变了物体上的(u,v)坐标,纹理坐标在GPU上达到了浮点精度的极限。同时,把地面的面积从1000减少到200也有帮助。

The annoying thing is the overhead from having all those extra faces in the plane geometry, even though they're completely redundant in specifying the shape.

令人讨厌的是,在平面几何中拥有所有这些额外的面,尽管它们在指定形状上是完全多余的。

In any case, the result looks exactly the same on my desktop browsers, but vastly better on my iPad 4.

无论如何,结果在我的桌面浏览器上看起来都是一样的,但在我的iPad 4上要好得多。

Edit: After more careful testing, I don't think subdividing the THREE.PlaneGeometry is making any difference, it's only reducing the overall size of the tiled plane that helps. And in fact, by making the size of the tiled plane large enough, whatever limit is being hit on the iPad 4 when the size is just 1000 can be reached on my iMac when the size is 80,000, as the second version of the code snippet shows. (The texture starts to degrade around 50,000, but 80,000 makes the distortion unmissable.) Obviously there are no real applications where you need to tile a surface with 50,000 x 50,000 copies of a texture, but a few hundred in each direction, which is where the iPad 4 starts to have problems, isn't extravagant.

编辑:经过更仔细的测试,我不认为会细分这三个。平面几何是有区别的,它只是减小了平面层的整体尺寸。事实上,通过将这台平板的大小调整到足够大的程度,当尺寸为8万时,在我的iMac上可以达到1000的上限,就像代码片段的第二版所显示的那样。(纹理开始降低约50,000,但80000使失真不可错过。)显然,没有真正的应用需要在一个表面贴上5万x 5万份纹理,但每个方向都要贴几百张,这正是iPad 4开始出现问题的地方,这并不奢侈。

First version of the code snippet, which fixes the problem on an 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>

Second version of the code snippet, which breaks the texture on a 2007 iMac:

第二个版本的代码片段,打破了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


1  

Greg Egan's observation makes a lot of sense. If you not only subdivide the plane but tile the UV coords so they repeat instead of using large numbers that might fix it.

格雷格·伊根的观察很有道理。如果你不仅细分平面,而且平铺UV coords所以他们重复而不是使用大数来修正它。

//	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>

#2


1  

I can't fully explain the source of the problem, but I've found a work-around that suggests that the cause is some kind of degradation of numerical precision, I guess in the GPU, that doesn't occur with every iPad graphics card.

我不能完全解释问题的根源,但我找到了一个解决办法,它表明问题的原因是某种数值精度的下降,我猜在GPU中,并不是所有iPad显卡都会出现这种情况。

The work-around involves splitting the plane geometry for the ground, which was originally just a single square (which three.js presumably divides into 2 triangles), into a grid of multiple squares. Presumably this changes something in the way the (u,v) coordinates on the object and the texture coordinates run up against the limits of floating point precision in the GPU. Also, reducing the size of the ground from 1000 to 200 helps.

围绕这个问题的解决方法是将地面的平面几何分割开来,这原本只是一个正方形(三个正方形)。js大概分成两个三角形),组成一个由多个正方形组成的网格。大概这改变了物体上的(u,v)坐标,纹理坐标在GPU上达到了浮点精度的极限。同时,把地面的面积从1000减少到200也有帮助。

The annoying thing is the overhead from having all those extra faces in the plane geometry, even though they're completely redundant in specifying the shape.

令人讨厌的是,在平面几何中拥有所有这些额外的面,尽管它们在指定形状上是完全多余的。

In any case, the result looks exactly the same on my desktop browsers, but vastly better on my iPad 4.

无论如何,结果在我的桌面浏览器上看起来都是一样的,但在我的iPad 4上要好得多。

Edit: After more careful testing, I don't think subdividing the THREE.PlaneGeometry is making any difference, it's only reducing the overall size of the tiled plane that helps. And in fact, by making the size of the tiled plane large enough, whatever limit is being hit on the iPad 4 when the size is just 1000 can be reached on my iMac when the size is 80,000, as the second version of the code snippet shows. (The texture starts to degrade around 50,000, but 80,000 makes the distortion unmissable.) Obviously there are no real applications where you need to tile a surface with 50,000 x 50,000 copies of a texture, but a few hundred in each direction, which is where the iPad 4 starts to have problems, isn't extravagant.

编辑:经过更仔细的测试,我不认为会细分这三个。平面几何是有区别的,它只是减小了平面层的整体尺寸。事实上,通过将这台平板的大小调整到足够大的程度,当尺寸为8万时,在我的iMac上可以达到1000的上限,就像代码片段的第二版所显示的那样。(纹理开始降低约50,000,但80000使失真不可错过。)显然,没有真正的应用需要在一个表面贴上5万x 5万份纹理,但每个方向都要贴几百张,这正是iPad 4开始出现问题的地方,这并不奢侈。

First version of the code snippet, which fixes the problem on an 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>

Second version of the code snippet, which breaks the texture on a 2007 iMac:

第二个版本的代码片段,打破了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>