How to draw CatmullRomCurve3(ClosedSplineCurve3) from bufferGeometry - javascript

For general lines with two or three vertices, I got help from #Wilt at the question: Can I merge geometry in every frame in rendering process using three.js?
So that I can generate a large number of lines with one single bufferGeometry.
using the code like:
var geometry = new THREE.BufferGeometry();
positions = new Float32Array(total * pointsNum * 3);
geometry.addAttribute(
'position', new THREE.BufferAttribute(positions, 3)
);
var material = new THREE.LineBasicMaterial({
transparent: true,
color: 0x0000ff,
opacity: 1
});
var line = new THREE.Line(bufferGeometry, material);
var all_vertices = [new THREE.Vector3(-1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 0, 0)];
for(var i = 0; i< all_vertices.length; i++){
positions[i] = all_vertices[i].x;
positions[i+1] = all_vertices[i].y;
positions[i+2] = all_vertices[i].z;
}
var start = all_vertices.length;
var end = start + all_vertices.length;
bufferGeometry.addGroup(start, end);
line.geometry.attributes.position.needsUpdate = true
What I was thinking is apparently too naive that I could get a CatmullRomCurve3 line by easily adding more vertices in var all_vertices. The fact is it is not that easy. For example, I got a few vertices like:
var maxX = Math.random() * 0.7 + 0.7;
var maxY = Math.random() * (maxX / 2) + 0.01;
var maxZ = Math.random() * (maxX - maxY) + maxY;
console.log(maxX + ' ' + maxY + ' ' + maxZ);
//32, 45, 38, 36, 35, 39, 41, 42
var degs = [32, 45, 38, 36, 35, 39, 41, 42];
var alpha = 32;
var tgAlpha = Math.tan(alpha);
var xpow = maxX * maxX;
var zpow = maxZ * maxZ;
orig[0] = new THREE.Vector3(-maxX, 0, 0);
var x1 = -Math.sqrt(xpow * zpow / (zpow + tgAlpha * xpow));
var z1 = x1 * tgAlpha;
orig[1] = new THREE.Vector3(x1, maxY, z1);
orig[2] = new THREE.Vector3(-x1, -maxY, z1);
orig[3] = new THREE.Vector3(maxX, 0, 0);
orig[4] = new THREE.Vector3(-x1, maxY, -z1);
orig[5] = new THREE.Vector3(x1, -maxY, -z1);
And apply them to var curve = new THREE.CatmullRomCurve3(orig); curve.closed = true;, As a result, I can get one closed line like a infinity sign in the 3D space.
So how can I use these vertices to create the CatmullRomCurve3 from bufferGeometry? Can I scale it to some other size?
The Jsfiddle is https://jsfiddle.net/do7ur33u/2/
Thanks for any help.

Related

when changing mesh in real time by three.js... (THREE.Object3D.add: object not an instance of THREE.Object3D)

I'm new to three.js and trying to change the mesh by clicking the button,
but I keep getting this error...
THREE.Object3D.add: object not an instance of THREE.Object3D.
I found that type of the mesh(ShapeGeometry) that I'm trying to change is the 'object', which is the same with the baseline object(SphereGeometry).
My codes are like this:
1. Baseline object (SphereGeometry)
var Geometry = new THREE.SphereGeometry(15, 15, 15);
var Material = new THREE.MeshLambertMaterial({
color: '#FFFFFF',
wireframe: true
});
var compoCenter = new THREE.Mesh(Geometry, Material);
var compoLeft = new THREE.Mesh(Geometry, Material);
var compoRight = new THREE.Mesh(Geometry, Material);
compoCenter.position.set(0, 0, 0);
compoLeft.position.set(-80, 0, 30);
compoRight.position.set(80, 0, 30);
group.add(compoCenter);
group.add(compoLeft);
group.add(compoRight);
render();
scene.add(group);
2. render() function
compoNew is an object that is exported from another file and it defines a new geometry that I want to change into.
function render() {
// color rendering
var userColor = document.querySelector('#userCustom').innerHTML;
Material.color = new THREE.Color(userColor);
// // Changing Geometry
if (shape_heart_cnt % 2 != 0) {
group.remove(compoCenter);
group.remove(compoLeft);
group.remove(compoRight);
var compoCenterNew = compoNew;
var compoLeftNew = compoNew;
var compoRightNew = compoNew;
scene.add(compoCenterNew);
compoCenterNew.position.set(0, 0, 0);
compoLeftNew.position.set(-80, 0, 30);
compoRightNew.position.set(80, 0, 30);
} else {
group.add(compoCenter);
group.add(compoLeft);
group.add(compoRight);
}
3. New Geometry(ShapeGeometry) that I want to change into
function decideShow(){
const x = 0;
const y = 0;
const heartShape = new THREE.Shape();
heartShape.moveTo( x + 5, y + 5 );
heartShape.bezierCurveTo( x + 5, y + 5, x + 4, y, x, y );
heartShape.bezierCurveTo( x - 6, y, x - 6, y + 7,x - 6, y + 7 );
heartShape.bezierCurveTo( x - 6, y + 11, x - 3, y + 15.4, x + 5, y + 19 );
heartShape.bezierCurveTo( x + 12, y + 15.4, x + 16, y + 11, x + 16, y + 7 );
heartShape.bezierCurveTo( x + 16, y + 7, x + 16, y, x + 10, y );
heartShape.bezierCurveTo( x + 7, y, x + 5, y + 5, x + 5, y + 5 );
var GeometryNew = new THREE.ShapeGeometry(heartShape);
var MaterialNew = new THREE.MeshBasicMaterial({
color: '#FFFFFF',
wireframe: true
});
var compoNew = new THREE.Mesh(GeometryNew, MaterialNew);
return compoNew
};
var compoNew = decideShow();
export { compoNew };
I think the point that I'm getting error is like the below (in render function). But I can't figure out why I'm getting THREE.Object3D.add: object not an instance of THREE.Object3D error.
scene.add(compoCenterNew);
Please Help me :(
The error is showing because you are trying to add a 2D object (ShapeGeometry) to a 3D Scene. Other than that, you need to show your code for compoNew so we can see what went wrong while exporting/importing the object into the new file.

Cylinder not srawing well when Y coordinate is different from 0. BabylonJs

I'm using babylonJs to draw some cylinders between 2 points and it works perfect when the points have the Y coord in 0, but when one of the points have a different Y, then the cylinder is off as you can see in the image
Error
This diagonal should go from corner to corner. The code I'm using is this one:
for(var i = 0;i+1 < newObj.attachmentPoints.length;i++){
if( newObj.attachmentPoints[i].attachmentType === "Measure" && newObj.attachmentPoints[i+1].attachmentType === "Measure")
{
let from = new BABYLON.Vector3(newObj.attachmentPoints[i].location.x, -newObj.attachmentPoints[i].location.y, -newObj.attachmentPoints[i].location.z);
let to = new BABYLON.Vector3(newObj.attachmentPoints[i+1].location.x, -newObj.attachmentPoints[i+1].location.y, -newObj.attachmentPoints[i+1].location.z);
const faceColors = [ ];
faceColors[0] = new BABYLON.Color4(0, 0, 1, 1);
faceColors[1] = new BABYLON.Color4(0, 0, 1, 1);
faceColors[2] = new BABYLON.Color4(0, 0, 1, 1);
let distance = BABYLON.Vector3.Distance(to,from );
var options = {
height:distance,
diameterTop:1,
diameterBottom:1,
tessellation:36,
faceColors:faceColors
}
var cylinder = BABYLON.MeshBuilder.CreateCylinder("cylinder", options, this._scene);
cylinder.setPivotMatrix(BABYLON.Matrix.Translation(0, -distance / 2, 0));
cylinder.position = new BABYLON.Vector3(newObj.attachmentPoints[i+1].location.x, -distance / 2, -newObj.attachmentPoints[i+1].location.z);
var v1 = to.subtract(from);
v1.normalize();
var v2 = new BABYLON.Vector3(0, 1, 0);
var axis = BABYLON.Vector3.Cross(v1, v2);
axis.normalize();
console.log(axis);
var angle = BABYLON.Vector3.Dot(v1, v2);
cylinder.rotationQuaternion = BABYLON.Quaternion.RotationAxis(axis, -Math.PI / 2 + angle);
}
}
Any clue?

(THREE.JS) calculate the custom UVs for custom Buffer Geometry

I am trying to create a curve wall using some vertices array in three JS.
What I will get in array is some base vertices in 2D which will be the bottom vertices of wall. These includes the center, lower and upper vertices which means it is two faced wall. based on these vertices, I added some wall height and convert that 2D to 3D.
Below is the code and working fiddle
/**
* Created by Singh on 7/30/2018.
*/
var renderer, scene, camera;
init();
animate();
function init() {
wallsGeometry = function(totalPoints){
var material = new THREE.MeshBasicMaterial({/*color: 0xff0000,*/ side: THREE.DoubleSide, wireframe : false});
var material2 = new THREE.MeshBasicMaterial({/*color: 0x0044400, */side: THREE.DoubleSide, wireframe : true});
var geometry = new THREE.BufferGeometry();
var geometry2 = new THREE.BufferGeometry();
var wallHeight = 200;
var pointindices = [
0,1,2,0,2,3, //left
5,4,7,5,7,6, //right
4,0,3,4,3,7, //back
1,5,6,1,6,2, //front
2,6,7,2,7,3, //top
5,1,0,5,0,4, //bottom
];
var pointindices2 = [
1,0,2,1,3,2 , //left
4,5,7,4,6,7, //right
0,4,3,0,7,3, //back
5,1,2,5,2,6, //front
6,2,7,6,3,7, //top
1,5,0,1,4,0, //bottom
];
var tempIndices = [];
var tempIndices2 = [];
for(var i=0; i<4; i++) {
for (var j = 0; j < pointindices.length; j++) {
tempIndices[pointindices.length * i + j] = 4 * i + pointindices[j];
}
}
for(var i=0; i<4; i++) {
for (var j = 0; j < pointindices2.length; j++) {
tempIndices2[pointindices2.length * i + j] = 4 * i + pointindices2[j];
}
}
var tempVerMesh = [];
var tempVerMesh2 = [];
var indices = new Uint32Array( tempIndices );
var pointsArray = { // for testing
pointUpper1: new THREE.Vector3(),
pointUpper2: new THREE.Vector3(),
pointCenter1: new THREE.Vector3(),
pointCenter2: new THREE.Vector3(),
pointLower1: new THREE.Vector3(),
pointLower2: new THREE.Vector3()
};
console.log(totalPoints);
/*function generateUVs(geometry) {
geometry.computeBoundingBox();
var max = geometry.boundingBox.max, min = geometry.boundingBox.min;
var offset = new THREE.Vector3(0 - min.x, 0 - min.y);
var range = new THREE.Vector3(max.x - min.x, max.y - min.y);
var faces = geometry.faces;
geometry.faceVertexUvs[0] = [];
for (var i = 0; i < faces.length ; i++) {
var v1 = geometry.vertices[faces[i].a],
v2 = geometry.vertices[faces[i].b],
v3 = geometry.vertices[faces[i].c];
geometry.faceVertexUvs[0].push([
new THREE.Vector3((v1.x + offset.x)/range.x ,(v1.y + offset.y)/range.y),
new THREE.Vector3((v2.x + offset.x)/range.x ,(v2.y + offset.y)/range.y),
new THREE.Vector3((v3.x + offset.x)/range.x ,(v3.y + offset.y)/range.y),
]);
}
geometry.uvsNeedUpdate = true;
return geometry;
}*/
for (var i = 0; i < totalPoints.lower.length ; i++) {
pointsArray.pointCenter1 = totalPoints.center[i];
//pointsArray.pointCenter2 = totalPoints.center[i + 1];
pointsArray.pointLower1 = totalPoints.lower[i];
//pointsArray.pointLower2 = totalPoints.lower[i + 1];
pointsArray.pointUpper1 = totalPoints.upper[i];
//pointsArray.pointUpper2 = totalPoints.upper[i + 1];
tempVerMesh.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y, pointsArray.pointCenter1.z);
tempVerMesh.push(pointsArray.pointLower1.x, pointsArray.pointLower1.y, pointsArray.pointLower1.z);
tempVerMesh.push(pointsArray.pointLower1.x, pointsArray.pointLower1.y + wallHeight, pointsArray.pointLower1.z);
tempVerMesh.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y + wallHeight, pointsArray.pointCenter1.z);
tempVerMesh2.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y, pointsArray.pointCenter1.z);
tempVerMesh2.push(pointsArray.pointUpper1.x, pointsArray.pointUpper1.y, pointsArray.pointUpper1.z);
tempVerMesh2.push(pointsArray.pointUpper1.x, pointsArray.pointUpper1.y + wallHeight, pointsArray.pointUpper1.z );
tempVerMesh2.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y + wallHeight, pointsArray.pointCenter1.z);
}
var vertices = new Float32Array(tempVerMesh);
var vertices2 = new Float32Array(tempVerMesh2);
//var uvs = new Float32Array(pointUVs);
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
//geometry.addAttribute('uv', new THREE.BufferAttribute(uvs, 2));
geometry.computeFaceNormals();
geometry.computeVertexNormals();
console.log(geometry);
var mesh = new THREE.Mesh(geometry, material);
var indices2 = new Uint32Array(tempIndices2);
geometry2.addAttribute('position', new THREE.BufferAttribute(vertices2, 3));
geometry2.setIndex(new THREE.BufferAttribute(indices2, 1));
geometry2.computeFaceNormals();
geometry2.computeVertexNormals();
/*var geom = new THREE.Geometry().fromBufferGeometry(geometry2);
var temp = generateUVs(geom);
geometry2 = new THREE.BufferGeometry().fromGeometry(temp);*/
var mesh2 = new THREE.Mesh(geometry2, material2);
//geometry2.addAttribute('uv', new THREE.BufferAttribute(uvs2, 2));
return [mesh,mesh2];
};
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 400;
scene = new THREE.Scene();
var arrow;
var rayCaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
renderer.domElement.addEventListener("mousemove", onMouseMove);
var points = {
pointUpper1: new THREE.Vector3(-70, 0, -20),
pointUpper2: new THREE.Vector3(130, 0, -20),
pointCenter1: new THREE.Vector3(-100, 0, 0),
pointCenter2: new THREE.Vector3(150, 0, 0),
pointLower1: new THREE.Vector3(-70, 0, 20),
pointLower2: new THREE.Vector3(130, 0, 20)
};
var totalPoints = {
center : [new THREE.Vector3(-70, 0, 0),new THREE.Vector3(0, 0, 0),new THREE.Vector3(50, 0, 0),new THREE.Vector3(100, 0, 0),new THREE.Vector3(130, 0, 0)],
lower : [new THREE.Vector3(-70, 0, 20),new THREE.Vector3(0, 0, 20),new THREE.Vector3(50, 0, 20),new THREE.Vector3(100, 0, 20),new THREE.Vector3(130, 0, 20)],
upper : [new THREE.Vector3(-70, 0, -20),new THREE.Vector3(0, 0, -20),new THREE.Vector3(50, 0, -20),new THREE.Vector3(100, 0, -20),new THREE.Vector3(130, 0, -20)]
};
var sphere = new THREE.SphereGeometry(2, 10, 10);
function initPoints(){
var point1mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({color: 0xff00}));
point1mesh.position.copy(points.pointUpper1);
scene.add(point1mesh);
var point2mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({color: 0x0000ff}));
point2mesh.position.copy(points.pointUpper2);
scene.add(point2mesh);
var point3mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({color: 0xff00}));
point3mesh.position.copy(points.pointCenter1);
scene.add(point3mesh);
var point4mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({color: 0x0000ff}));
point4mesh.position.copy(points.pointCenter2);
scene.add(point4mesh);
var point5mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({color: 0xff00}));
point5mesh.position.copy(points.pointLower1);
scene.add(point5mesh);
var point6mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({color: 0x0000ff}));
point6mesh.position.copy(points.pointLower2);
scene.add(point6mesh);
}
initPoints();
var mesh = new wallsGeometry(totalPoints);
function createArrow() {
var length = 30;
arrow = new THREE.ArrowHelper(
THREE.Object3D.DefaultUp,
new THREE.Vector3(),
length,
0xffff00,
1.5 * length,
1.25 * length
);
arrow.position.z = 10;
scene.add(arrow);
}
// arrow
createArrow();
function updateArrow(object, point, face) {
arrow.position.copy(point);
var normalMatrix = new THREE.Matrix3().getNormalMatrix( object.matrixWorld );
var normal = face.normal.clone().applyMatrix3( normalMatrix ).normalize();
arrow.setDirection(normal);
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
rayCaster.setFromCamera(mouse, camera);
var intersects = rayCaster.intersectObject(mesh[0], true);
var i, il, intersect;
if (intersects[0]) {
var face = intersects[0].face;
var point = intersects[0].point;
var object = intersects[0].object;
updateArrow(object, point, face);
}
}
/* var textureLoader = new THREE.TextureLoader();
textureLoader.load('./textures/Capture.PNG', function (texture) {
console.log('texture loaded');
texture.minFilter = THREE.LinearFilter;
//mesh[0].material.map = texture;
var vertexNormalsHelper = new THREE.VertexNormalsHelper( mesh[0], 10 );
mesh[0].add( vertexNormalsHelper );
}); */
scene.add(mesh[0]);
/* textureLoader.load('./textures/abc.jpg', function (texture) {
console.log('texture loaded');
texture.minFilter = THREE.LinearFilter;
//mesh[1].material.map = texture;
var vertexNormalsHelper = new THREE.VertexNormalsHelper( mesh[1], 10 );
mesh[1].add( vertexNormalsHelper );
}); */
scene.add(mesh[1]);
//
var Orbitcontrols = new THREE.OrbitControls(camera,renderer.domElement);
Orbitcontrols.update();
}
// render
function render() {
renderer.render(scene, camera);
}
// animate
function animate() {
requestAnimationFrame(animate);
render();
}
https://jsfiddle.net/simar_aneja/fsmw8znq/6/
In the fiddle you can see that wall is building properly and you can increase the vertices and loop of creating those indices, in the start. Now I want to add UVs to this bufferGeometry, I tried converting to geometry and then calculated faceVertexUVs, but this is not the right way. Can anyone suggest me the further path where I can attach different textures only at the front side, different on top side. And it should be in such a way that no matter how many vertices comes, Uvs should get calculated based on length of vertices. I have some Idea but not getting how to make it different for diff sides of wall.
Thanks
Here's a UV box-unwrapping I fixed up for ya. Maybe you'll find it helpful...
I also put your fiddle into a snippet you can run below...
function boxUnwrapUVs(geometry) {
if (!geometry.boundingBox) geometry.computeBoundingBox();
var sz = geometry.boundingBox.getSize(new THREE.Vector3());
var center = geometry.boundingBox.getCenter(new THREE.Vector3())
var min = geometry.boundingBox.min;
if (geometry.faceVertexUvs[0].length == 0) {
for (var i = 0; i < geometry.faces.length; i++) {
geometry.faceVertexUvs[0].push([new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()]);
}
}
for (var i = 0; i < geometry.faces.length; i++) {
var face = geometry.faces[i];
var faceUVs = geometry.faceVertexUvs[0][i]
var va = geometry.vertices[geometry.faces[i].a]
var vb = geometry.vertices[geometry.faces[i].b]
var vc = geometry.vertices[geometry.faces[i].c]
var vab = new THREE.Vector3().copy(vb).sub(va)
var vac = new THREE.Vector3().copy(vc).sub(va)
//now we have 2 vectors to get the cross product of...
var vcross = new THREE.Vector3().copy(vab).cross(vac);
//Find the largest axis of the plane normal...
vcross.set(Math.abs(vcross.x), Math.abs(vcross.y), Math.abs(vcross.z))
var majorAxis = vcross.x > vcross.y ? (vcross.x > vcross.z ? 'x' : vcross.y > vcross.z ? 'y' : vcross.y > vcross.z) : vcross.y > vcross.z ? 'y' : 'z'
//Take the other two axis from the largest axis
var uAxis = majorAxis == 'x' ? 'y' : majorAxis == 'y' ? 'x' : 'x';
var vAxis = majorAxis == 'x' ? 'z' : majorAxis == 'y' ? 'z' : 'y';
faceUVs[0].set((va[uAxis] - min[uAxis]) / sz[uAxis], (va[vAxis] - min[vAxis]) / sz[vAxis])
faceUVs[1].set((vb[uAxis] - min[uAxis]) / sz[uAxis], (vb[vAxis] - min[vAxis]) / sz[vAxis])
faceUVs[2].set((vc[uAxis] - min[uAxis]) / sz[uAxis], (vc[vAxis] - min[vAxis]) / sz[vAxis])
}
geometry.elementsNeedUpdate = geometry.verticesNeedUpdate = true;
}
geometry = new THREE.Geometry().fromBufferGeometry(geometry)
boxUnwrapUVs(geometry)
var mesh = new THREE.Mesh(geometry, material);
/**
* Created by Singh on 7/30/2018.
*/
var renderer, scene, camera;
init();
animate();
function init() {
wallsGeometry = function(totalPoints) {
var rrnd = (min, max) => (Math.random() * (max - min)) + min
var irnd = (rng) => (Math.random() * rng) | 0
function makeRndCanvas() {
var canvas = document.createElement('canvas');
canvas.width = canvas.height = 128;
var ctx = canvas.getContext('2d');
var srnd = (rng) => (Math.random() - 0.5) * 2 * rng
var irnd = (rng) => ((Math.random() * rng) | 0)
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.width; y++) {
ctx.fillStyle = `rgba(${irnd(256)},${irnd(256)},${irnd(256)},1.0)`
ctx.fillRect(x, y, 1, 1);
}
}
ctx.fillStyle = '#ffff00'
ctx.fillText("WAHOO", 3, 64)
return canvas;
}
function makeRndTexture() {
var tex = new THREE.Texture(makeRndCanvas())
tex.minFilter = // THREE.LinearMipMapLinearFilter;
tex.magFilter = THREE.NearestFilter; //THREE.LinearFilter;
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
//tex.repeat.set(0.01, 0.01);
tex.needsUpdate = true;
return tex;
}
var material = new THREE.MeshLambertMaterial({ /*color: 0xff0000,*/
side: THREE.DoubleSide,
wireframe: false,
map: makeRndTexture()
});
var material2 = new THREE.MeshLambertMaterial({ /*color: 0x0044400, */
side: THREE.DoubleSide,
wireframe: true
});
var geometry = new THREE.BufferGeometry();
var geometry2 = new THREE.BufferGeometry();
var wallHeight = 200;
var pointindices = [
0, 1, 2, 0, 2, 3, //left
5, 4, 7, 5, 7, 6, //right
4, 0, 3, 4, 3, 7, //back
1, 5, 6, 1, 6, 2, //front
2, 6, 7, 2, 7, 3, //top
5, 1, 0, 5, 0, 4, //bottom
];
var pointindices2 = [
1, 0, 2, 1, 3, 2, //left
4, 5, 7, 4, 6, 7, //right
0, 4, 3, 0, 7, 3, //back
5, 1, 2, 5, 2, 6, //front
6, 2, 7, 6, 3, 7, //top
1, 5, 0, 1, 4, 0, //bottom
];
var tempIndices = [];
var tempIndices2 = [];
for (var i = 0; i < 4; i++) {
for (var j = 0; j < pointindices.length; j++) {
tempIndices[pointindices.length * i + j] = 4 * i + pointindices[j];
}
}
for (var i = 0; i < 4; i++) {
for (var j = 0; j < pointindices2.length; j++) {
tempIndices2[pointindices2.length * i + j] = 4 * i + pointindices2[j];
}
}
var tempVerMesh = [];
var tempVerMesh2 = [];
var indices = new Uint32Array(tempIndices);
var pointsArray = { // for testing
pointUpper1: new THREE.Vector3(),
pointUpper2: new THREE.Vector3(),
pointCenter1: new THREE.Vector3(),
pointCenter2: new THREE.Vector3(),
pointLower1: new THREE.Vector3(),
pointLower2: new THREE.Vector3()
};
console.log(totalPoints);
/*function generateUVs(geometry) {
geometry.computeBoundingBox();
var max = geometry.boundingBox.max, min = geometry.boundingBox.min;
var offset = new THREE.Vector3(0 - min.x, 0 - min.y);
var range = new THREE.Vector3(max.x - min.x, max.y - min.y);
var faces = geometry.faces;
geometry.faceVertexUvs[0] = [];
for (var i = 0; i < faces.length ; i++) {
var v1 = geometry.vertices[faces[i].a],
v2 = geometry.vertices[faces[i].b],
v3 = geometry.vertices[faces[i].c];
geometry.faceVertexUvs[0].push([
new THREE.Vector3((v1.x + offset.x)/range.x ,(v1.y + offset.y)/range.y),
new THREE.Vector3((v2.x + offset.x)/range.x ,(v2.y + offset.y)/range.y),
new THREE.Vector3((v3.x + offset.x)/range.x ,(v3.y + offset.y)/range.y),
]);
}
geometry.uvsNeedUpdate = true;
return geometry;
}*/
for (var i = 0; i < totalPoints.lower.length; i++) {
pointsArray.pointCenter1 = totalPoints.center[i];
//pointsArray.pointCenter2 = totalPoints.center[i + 1];
pointsArray.pointLower1 = totalPoints.lower[i];
//pointsArray.pointLower2 = totalPoints.lower[i + 1];
pointsArray.pointUpper1 = totalPoints.upper[i];
//pointsArray.pointUpper2 = totalPoints.upper[i + 1];
tempVerMesh.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y, pointsArray.pointCenter1.z);
tempVerMesh.push(pointsArray.pointLower1.x, pointsArray.pointLower1.y, pointsArray.pointLower1.z);
tempVerMesh.push(pointsArray.pointLower1.x, pointsArray.pointLower1.y + wallHeight, pointsArray.pointLower1.z);
tempVerMesh.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y + wallHeight, pointsArray.pointCenter1.z);
tempVerMesh2.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y, pointsArray.pointCenter1.z);
tempVerMesh2.push(pointsArray.pointUpper1.x, pointsArray.pointUpper1.y, pointsArray.pointUpper1.z);
tempVerMesh2.push(pointsArray.pointUpper1.x, pointsArray.pointUpper1.y + wallHeight, pointsArray.pointUpper1.z);
tempVerMesh2.push(pointsArray.pointCenter1.x, pointsArray.pointCenter1.y + wallHeight, pointsArray.pointCenter1.z);
}
var vertices = new Float32Array(tempVerMesh);
var vertices2 = new Float32Array(tempVerMesh2);
//var uvs = new Float32Array(pointUVs);
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
//geometry.addAttribute('uv', new THREE.BufferAttribute(uvs, 2));
geometry.computeFaceNormals();
geometry.computeVertexNormals();
console.log(geometry);
function boxUnwrapUVs(geometry) {
if (!geometry.boundingBox) geometry.computeBoundingBox();
var sz = geometry.boundingBox.getSize(new THREE.Vector3());
var center = geometry.boundingBox.getCenter(new THREE.Vector3())
var min = geometry.boundingBox.min;
if (geometry.faceVertexUvs[0].length == 0) {
for (var i = 0; i < geometry.faces.length; i++) {
geometry.faceVertexUvs[0].push([new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()]);
}
}
for (var i = 0; i < geometry.faces.length; i++) {
var face = geometry.faces[i];
var faceUVs = geometry.faceVertexUvs[0][i]
var va = geometry.vertices[geometry.faces[i].a]
var vb = geometry.vertices[geometry.faces[i].b]
var vc = geometry.vertices[geometry.faces[i].c]
var vab = new THREE.Vector3().copy(vb).sub(va)
var vac = new THREE.Vector3().copy(vc).sub(va)
//now we have 2 vectors to get the cross product of...
var vcross = new THREE.Vector3().copy(vab).cross(vac);
//Find the largest axis of the plane normal...
vcross.set(Math.abs(vcross.x), Math.abs(vcross.y), Math.abs(vcross.z))
var majorAxis = vcross.x > vcross.y ? (vcross.x > vcross.z ? 'x' : vcross.y > vcross.z ? 'y' : vcross.y > vcross.z) : vcross.y > vcross.z ? 'y' : 'z'
//Take the other two axis from the largest axis
var uAxis = majorAxis == 'x' ? 'y' : majorAxis == 'y' ? 'x' : 'x';
var vAxis = majorAxis == 'x' ? 'z' : majorAxis == 'y' ? 'z' : 'y';
faceUVs[0].set((va[uAxis] - min[uAxis]) / sz[uAxis], (va[vAxis] - min[vAxis]) / sz[vAxis])
faceUVs[1].set((vb[uAxis] - min[uAxis]) / sz[uAxis], (vb[vAxis] - min[vAxis]) / sz[vAxis])
faceUVs[2].set((vc[uAxis] - min[uAxis]) / sz[uAxis], (vc[vAxis] - min[vAxis]) / sz[vAxis])
}
geometry.elementsNeedUpdate = geometry.verticesNeedUpdate = true;
}
geometry = new THREE.Geometry().fromBufferGeometry(geometry)
boxUnwrapUVs(geometry)
geometry = new THREE.BufferGeometry().fromGeometry(geometry)
var mesh = new THREE.Mesh(geometry, material);
var indices2 = new Uint32Array(tempIndices2);
geometry2.addAttribute('position', new THREE.BufferAttribute(vertices2, 3));
geometry2.setIndex(new THREE.BufferAttribute(indices2, 1));
geometry2.computeFaceNormals();
geometry2.computeVertexNormals();
/* var geom = new THREE.Geometry().fromBufferGeometry(geometry2);
var temp = generateUVs(geom);
geometry2 = new THREE.BufferGeometry().fromGeometry(temp);*/
var mesh2 = new THREE.Mesh(geometry2, material2);
//geometry2.addAttribute('uv', new THREE.BufferAttribute(uvs2, 2));
return [mesh, mesh2];
};
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 400;
scene = new THREE.Scene();
var arrow;
var rayCaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
var light = new THREE.DirectionalLight();
light.position.set(200, 200, 200)
scene.add(light)
document.body.appendChild(renderer.domElement);
renderer.domElement.addEventListener("mousemove", onMouseMove);
var points = {
pointUpper1: new THREE.Vector3(-70, 0, -20),
pointUpper2: new THREE.Vector3(130, 0, -20),
pointCenter1: new THREE.Vector3(-100, 0, 0),
pointCenter2: new THREE.Vector3(150, 0, 0),
pointLower1: new THREE.Vector3(-70, 0, 20),
pointLower2: new THREE.Vector3(130, 0, 20)
};
var totalPoints = {
center: [new THREE.Vector3(-70, 0, 0), new THREE.Vector3(0, 0, 0), new THREE.Vector3(50, 0, 0), new THREE.Vector3(100, 0, 0), new THREE.Vector3(130, 0, 0)],
lower: [new THREE.Vector3(-70, 0, 20), new THREE.Vector3(0, 0, 20), new THREE.Vector3(50, 0, 20), new THREE.Vector3(100, 0, 20), new THREE.Vector3(130, 0, 20)],
upper: [new THREE.Vector3(-70, 0, -20), new THREE.Vector3(0, 0, -20), new THREE.Vector3(50, 0, -20), new THREE.Vector3(100, 0, -20), new THREE.Vector3(130, 0, -20)]
};
var sphere = new THREE.SphereGeometry(2, 10, 10);
function initPoints() {
var point1mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({
color: 0xff00
}));
point1mesh.position.copy(points.pointUpper1);
scene.add(point1mesh);
var point2mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({
color: 0x0000ff
}));
point2mesh.position.copy(points.pointUpper2);
scene.add(point2mesh);
var point3mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({
color: 0xff00
}));
point3mesh.position.copy(points.pointCenter1);
scene.add(point3mesh);
var point4mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({
color: 0x0000ff
}));
point4mesh.position.copy(points.pointCenter2);
scene.add(point4mesh);
var point5mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({
color: 0xff00
}));
point5mesh.position.copy(points.pointLower1);
scene.add(point5mesh);
var point6mesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({
color: 0x0000ff
}));
point6mesh.position.copy(points.pointLower2);
scene.add(point6mesh);
}
initPoints();
var mesh = new wallsGeometry(totalPoints);
function createArrow() {
var length = 30;
arrow = new THREE.ArrowHelper(
THREE.Object3D.DefaultUp,
new THREE.Vector3(),
length,
0xffff00,
1.5 * length,
1.25 * length
);
arrow.position.z = 10;
scene.add(arrow);
}
// arrow
createArrow();
function updateArrow(object, point, face) {
arrow.position.copy(point);
var normalMatrix = new THREE.Matrix3().getNormalMatrix(object.matrixWorld);
var normal = face.normal.clone().applyMatrix3(normalMatrix).normalize();
arrow.setDirection(normal);
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
rayCaster.setFromCamera(mouse, camera);
var intersects = rayCaster.intersectObject(mesh[0], true);
var i, il, intersect;
if (intersects[0]) {
var face = intersects[0].face;
var point = intersects[0].point;
var object = intersects[0].object;
updateArrow(object, point, face);
}
}
/* var textureLoader = new THREE.TextureLoader();
textureLoader.load('./textures/Capture.PNG', function (texture) {
console.log('texture loaded');
texture.minFilter = THREE.LinearFilter;
//mesh[0].material.map = texture;
var vertexNormalsHelper = new THREE.VertexNormalsHelper( mesh[0], 10 );
mesh[0].add( vertexNormalsHelper );
}); */
scene.add(mesh[0]);
/* textureLoader.load('./textures/abc.jpg', function (texture) {
console.log('texture loaded');
texture.minFilter = THREE.LinearFilter;
//mesh[1].material.map = texture;
var vertexNormalsHelper = new THREE.VertexNormalsHelper( mesh[1], 10 );
mesh[1].add( vertexNormalsHelper );
}); */
scene.add(mesh[1]);
//
var Orbitcontrols = new THREE.OrbitControls(camera, renderer.domElement);
Orbitcontrols.update();
}
// render
function render() {
renderer.render(scene, camera);
}
// animate
function animate() {
requestAnimationFrame(animate);
render();
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>

Is it possible to hover on the pieces of a LineSegment distinctly in three.js to identify those with only one geometry?

I have created two examples of drawing lines in three.js, one uses different geometry and material and another uses only one geometry and one material. The links of the fidlles are: https://jsfiddle.net/sounakgarai/6reawwot/4/ and https://jsfiddle.net/sounakgarai/2xL70me3/ respectively.
I can hover on the lines in the first example and can see them change the material color on mouseover. And in the second example when I hover, the whole lineMesh gets its color changed as there is only one material.
Now what I would like to do is to use only one geometry like in the second example. But I want to see an effect which is seen in the first example: all the different pieces of the LineSegment (of the second example) I want to see those getting hovered in a way so that I can identify those distinctly (but I don't want to use different geometry for that).
Is it possible to achieve?
The important thing is that when I use multiple geometry, it makes the browser hang and go slow while rendering a large model which has considerably large number of members. And I also want to render multiple three.js models in different div elements in a page in future. So it may make the browser go more slow while rendering them.
An option with THREE.BufferGeometry(), THREE.LineSegments() and .vertexColors = THREE.VertexColors:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, 10);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var points = [
new THREE.Vector3(-5, 5, 0),
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(5, 5, 0),
new THREE.Vector3(5, 5, 0),
new THREE.Vector3(5, -5, 0)
];
var geom = new THREE.BufferGeometry().setFromPoints(points);
geom.addAttribute("color", new THREE.BufferAttribute(new Float32Array([1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, .5, .5, .5, 1, 0, 1]), 3)); // we'll change this color attribute
geom.addAttribute("colorBase", new THREE.BufferAttribute(new Float32Array([1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, .5, .5, .5, 1, 0, 1]), 3)); // this attribute contains original colors for restoring
var mat = new THREE.LineBasicMaterial({
vertexColors: THREE.VertexColors
});
var line = new THREE.LineSegments(geom, mat);
scene.add(line);
// all of those variables are for re-use
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var intersects = [];
var oldIndex = -1;
var col = new THREE.Color();
window.addEventListener("mousemove", onMouseMove, false);
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
intersects = raycaster.intersectObject(line);
if (intersects.length === 0) return;
let idx = intersects[0].index;
if (idx !== oldIndex) highlightSegment(idx, 0xFFFF00);
}
function highlightSegment(idx, color) {
setColor(idx, color);
if (oldIndex !== -1) {
restoreColor();
}
line.geometry.attributes.color.needsUpdate = true;
oldIndex = idx; // save current index as an old one
}
function setColor(idx, color) { // change color for the current segment
let idxNear = idx % 2 === 0 ? idx + 1 : idx - 1; //
// if 'idx' is an index of the start point in a segment, then its pair will be idx + 1,
// otherwise is idx - 1.
col.set(color);
let colors = line.geometry.attributes.color;
colors.setXYZ(idx, col.r, col.g, col.b); // a very useful method of 'THREE.BufferAttribute()'
colors.setXYZ(idxNear, col.r, col.g, col.b);
}
function restoreColor() { // restore the original color for the old segment
let oldIndexNear = oldIndex % 2 === 0 ? oldIndex + 1 : oldIndex - 1;
let colors = line.geometry.attributes.color;
let colorBase = line.geometry.attributes.colorBase;
colors.copyAt(oldIndex, colorBase, oldIndex); // another useful method of 'THREE.BufferAttribute()'
colors.copyAt(oldIndexNear, colorBase, oldIndexNear);
}
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://threejs.org/build/three.min.js"></script>
You have to provide vertex colors instead of the material color. Like this -
var lineMaterial = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors});
And for each vertex, push the corresponding color. Like this -
geometry.colors.push(new THREE.Color("rgb(0, 0, 255)"), new THREE.Color("rgb(0, 0, 255)"));
And in the mouse hover handler, you need to get the index of the currently intersected line and change the color of that vertex. Like this -
intersects[0].object.geometry.colors[intersects[0].index].setHex(0xff0000);
let nextIndex = intersects[0].index + (intersects[0].index % 2 == 0 ? 1 : -1 );
intersects[0].object.geometry.colors[nextIndex].setHex(0xff0000);
intersects[0].object.geometry.colorsNeedUpdate = true;
Here is the simplified code based on your second version -
var container, scene, camera, renderer;
var mouse = new THREE.Vector2();
var intersected;
init();
animate();
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function init() {
container = document.getElementById("myDiv");
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf7f7f7);
var WIDTH = container.clientWidth,
HEIGHT = container.clientHeight;
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(WIDTH, HEIGHT);
container.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(45, WIDTH / HEIGHT, 0.1, 20000);
camera.up.set(0, 0, 1);
camera.lookAt(scene.position);
camera.position.set(0, 0, 100);
var nsize = 1, nspace = 5, nrange;
nrange = nsize * nspace;
var i, j, x, y, z = 0;
var lineMaterial = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors});
var geometry = new THREE.Geometry();
for (i = 0; i <= nsize; i++) {
for (j = 0, x = 0, y = 0; j <= nsize; j++) {
geometry.vertices.push(new THREE.Vector3(x, y, z), new THREE.Vector3(x, y + nspace, z));
geometry.colors.push(new THREE.Color("rgb(0, 0, 255)"), new THREE.Color("rgb(0, 0, 255)"));
x += nspace;
}
for (j = 0, x = 0, y = 0; j <= nsize; j++) {
geometry.vertices.push(new THREE.Vector3(x, y, z), new THREE.Vector3(x + nspace, y, z));
geometry.colors.push(new THREE.Color("rgb(0, 0, 255)"), new THREE.Color("rgb(0, 0, 255)"));
y += nspace;
}
z += nspace;
}
y = -10;
for (i = 0; i <= nsize; i++) {
for (j = 0, x = 0, z = 0; j <= nsize; j++) {
geometry.vertices.push(new THREE.Vector3(x, y, z), new THREE.Vector3(x, y, z + nspace));
geometry.colors.push(new THREE.Color("rgb(0, 0, 255)"), new THREE.Color("rgb(0, 0, 255)"));
x += nspace;
}
for (j = 0, x = 0, z = 0; j <= nsize; j++) {
geometry.vertices.push(new THREE.Vector3(x, y, z), new THREE.Vector3(x + nspace, y, z));
geometry.colors.push(new THREE.Color("rgb(0, 0, 255)"), new THREE.Color("rgb(0, 0, 255)"));
z += nspace;
}
y += nspace;
}
var lineMesh = new THREE.LineSegments(geometry, lineMaterial);
lineMesh.name = guid();
lineMesh.position.x = -(nsize * nspace) / 2;
lineMesh.position.y = -(nsize * nspace) / 2;
lineMesh.position.z = -(nsize * nspace) / 2;
scene.add(lineMesh);
}
function animate() {
requestAnimationFrame(animate);
// Render the scene.
render();
}
function onMouseMove(event) {
event.preventDefault();
// calculate mouse position in normalized device coordinates
// (-1 to +1) for both components
mouse.x = (event.clientX / container.clientWidth) * 2 - 1;
mouse.y = - (event.clientY / container.clientHeight) * 2 + 1;
if (mouse !== null && camera !== null) {
var ray = new THREE.Raycaster();
ray.setFromCamera(mouse, camera);
var intersects = ray.intersectObjects(scene.children);
if (intersects.length > 0) {
intersects[0].object.geometry.colors[intersects[0].index].setHex(0xff0000);
let nextIndex = intersects[0].index + (intersects[0].index % 2 == 0 ? 1 : -1 );
intersects[0].object.geometry.colors[nextIndex].setHex(0xff0000);
intersects[0].object.geometry.colorsNeedUpdate = true;
}
}
}
function render() {
renderer.render(scene, camera);
}
container.addEventListener('mousemove', onMouseMove, false);
Alternative of the raycaster, you can use GPU picking to identify the intersected line.
It is not possible to change the material of a single LineSegment if you use a single geometry, but what you could do is overlay an extra LineSegment added dynamically on top of the segment you want to highlight, which I think will produce the effect you are looking for.
You can achieve that by creating a material with no depthTest:
material.depthTest=false
Which will always appear on top of the geometry that has a material with depthTest.

Flaw in iOS WebGL texture rendering

This simple test of WebGL texture rendering using the three.js library:
// 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.
Desktop
iPad
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.
texture:
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.
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?
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.
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.
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.
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.
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.
First version of the code snippet, which fixes the problem on an 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:
// 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>
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.
// 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>

Categories