I'm having headaches when trying to extrude a spline to scene's origin. Here's what I'm trying to do :
I'm creating a spline with
let centerX = 0
let centerY = 0
let radius = 200
let coils = 50
let rotation = 2 * Math.PI
let thetaMax = coils * Math.PI
let awayStep = radius / thetaMax
let chord = 5
let vertices = []
for (let theta = chord / awayStep; theta <= thetaMax;) {
let away = awayStep * theta
let around = theta + rotation
let x = centerX + Math.cos(around) * away
let y = centerY + Math.sin(around) * away
theta += chord / away
let vec3 = new THREE.Vector3(x, y, 0)
vertices.push(vec3)
}
let axisPoints = []
data.forEach((d, i) => {
axisPoints.push(new THREE.Vector3(vertices[i].z, vertices[i].y / 2, vertices[i].x / 2))
})
let axis = new THREE.CatmullRomCurve3(axisPoints)
let geometry = new THREE.BufferGeometry().setFromPoints(axis.getPoints(axisPoints.length))
let material = new THREE.LineBasicMaterial({
color: 0x481e34,
transparent: true,
opacity: 0.2
})
let splineObject = new THREE.Line(geometry, material)
scene.add(splineObject)
And I want to extrude a "mesh"/"face" along this spline which is going right to the scene's origin (0) exactly like this :
I've tried many things but I can't figure it out :(
Any help would be very appreciated!
Thanks!
As an option, you can modify arrays of coordinates of an open-ended cylinder buffer geometry with coordinates of your spline:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 5, 10);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
scene.add(new THREE.GridHelper(10, 10, "white"));
var divisions = 100;
var points = [
new THREE.Vector3(-5, 5, 0),
new THREE.Vector3(-2, 2, 3),
new THREE.Vector3(1, 3, 2),
new THREE.Vector3(3, 5, -3),
new THREE.Vector3(-3, 4, -2)
];
var curve = new THREE.CatmullRomCurve3(points);
curve.closed = true;
var upperPoints = curve.getPoints(divisions);
var lowerPoints = upperPoints.map(p => {
return new THREE.Vector3(p.x, 0, p.z)
});
var upperGeom = new THREE.BufferGeometry().setFromPoints(upperPoints);
var lowerGeom = new THREE.BufferGeometry().setFromPoints(lowerPoints);
var cylGeom = new THREE.CylinderBufferGeometry(1, 1, 1, divisions, 1, true); // create an open-ended cylinder
cylGeom.attributes.position.array.set(upperGeom.attributes.position.array, 0); // set coordinates for upper points
cylGeom.attributes.position.array.set(lowerGeom.attributes.position.array, (divisions + 1) * 3); // set coordinates of lower points
cylGeom.computeVertexNormals();
var result = new THREE.Mesh(cylGeom, new THREE.MeshBasicMaterial({
color: 0xaaaaaa,
wireframe: true
}));
scene.add(result);
var upperLine = new THREE.Line(upperGeom, new THREE.LineBasicMaterial({
color: "aqua"
}));
scene.add(upperLine);
var lowerLine = new THREE.Line(lowerGeom, new THREE.LineBasicMaterial({
color: "yellow"
}));
scene.add(lowerLine);
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
body {
overflow: hidden;
margin: 0;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
Depends on your needs, you can modify points of any primitive.
Trying to paint each cube face with a different color, I found a thread that presents a way to achieve this:
var geometry = new THREE.BoxGeometry(5, 5, 5);
for (var i = 0; i < geometry.faces.length; i++) {
geometry.faces[i].color.setHex(Math.random() * 0xffffff);
}
var material = new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors
});
But with three.js r86, I get the following result:
Got the triangles that make up each face, painted individually.
To achieve the desirable effect, I used the following adaptation of the above code:
var geometry = new THREE.BoxGeometry(5, 5, 5);
for ( var i = 0; i < geometry.faces.length; i += 2 ) {
var faceColor = Math.random() * 0xffffff;
geometry.faces[i].color.setHex(faceColor);
geometry.faces[i+1].color.setHex(faceColor);
}
var material = new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors
});
But this all seems a bit over worked!
'use strict';
var camera, scene, renderer, cube;
init();
render();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// renderer
renderer = new THREE.WebGLRenderer({
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.z = 12;
// Mesh - cube
var geometry = new THREE.BoxGeometry(5, 5, 5);
for (var i = 0; i < geometry.faces.length; i += 2) {
var faceColor = Math.random() * 0xffffff;
geometry.faces[i].color.setHex(faceColor);
geometry.faces[i + 1].color.setHex(faceColor);
}
var material = new THREE.MeshBasicMaterial({
color: 0xffffff,
vertexColors: THREE.FaceColors
});
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// Light
var pointLight = new THREE.PointLight(0xFFFFFF);
pointLight.position.x = 10;
pointLight.position.y = 50;
pointLight.position.z = 130;
scene.add(pointLight);
}
function render() {
cube.rotation.x = 16;
cube.rotation.y = 4;
cube.rotation.z -= 5;
renderer.render(scene, camera);
}
body,
canvas {
margin: 0;
padding: 0;
}
body {
overflow: hidden;
background-color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/86/three.js"></script>
Am I missing something on three.js to accomplish the face painting as a whole ?
If you switch to BufferGeometry you can use groups to control the material of sections of your geometry. Groups are based on the vertex indices, and allow you to define a material index, which will reference a material inside an array of materials.
Consider:
// start, count, material index
bufferGeometry.addGroup(12, 6, 2)
This tells the geometry to start a new group of triangles at indices index 12, and accounts for 6 indices (which reference 6 vertices). The final parameter tells the group of triangles to use material index 2 (index 2 of the array of materials you use to create the mesh).
In the example below, I've given each side of a cube a different color. You might think this is the same effect as setting face colors, but note that this is setting a material per group, not just a color, which can lead to creating some really cool effects.
var renderer, scene, camera, controls, stats, mesh;
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 35,
NEAR = 1,
FAR = 1000;
function populateScene() {
var bg = new THREE.BufferGeometry();
bg.addAttribute("position", new THREE.BufferAttribute(new Float32Array([
// front
-1, 1, 1, // 0
-1, -1, 1, // 1
1, 1, 1, // 2
1, -1, 1, // 3
// right
1, 1, 1, // 4
1, -1, 1, // 5
1, 1, -1, // 6
1, -1, -1, // 7
// back
1, 1, -1, // 8
1, -1, -1, // 9
-1, 1, -1, // 10
-1, -1, -1, // 11
// left
-1, 1, -1, // 12
-1, -1, -1, // 13
-1, 1, 1, // 14
-1, -1, 1, // 15
// top
-1, 1, -1, // 16
-1, 1, 1, // 17
1, 1, -1, // 18
1, 1, 1, // 19
// bottom
-1, -1, 1, // 20
-1, -1, -1, // 21
1, -1, 1, // 22
1, -1, -1 // 23
]), 3));
bg.addAttribute("normal", new THREE.BufferAttribute(new Float32Array([
// front
0, 0, 1, // 0
0, 0, 1, // 1
0, 0, 1, // 2
0, 0, 1, // 3
// right
1, 0, 0, // 4
1, 0, 0, // 5
1, 0, 0, // 6
1, 0, 0, // 7
// back
0, 0, -1, // 8
0, 0, -1, // 9
0, 0, -1, // 10
0, 0, -1, // 11
// left
-1, 0, 0, // 12
-1, 0, 0, // 13
-1, 0, 0, // 14
-1, 0, 0, // 15
// top
0, 1, 0, // 16
0, 1, 0, // 17
0, 1, 0, // 18
0, 1, 0, // 19
// bottom
0, -1, 0, // 20
0, -1, 0, // 21
0, -1, 0, // 22
0, -1, 0 // 23
]), 3));
bg.setIndex(new THREE.BufferAttribute(new Uint32Array([
// front 0
0, 1, 2,
3, 2, 1,
// right 6
4, 5, 6,
7, 6, 5,
// back 12
8, 9, 10,
11, 10, 9,
// left 18
12, 13, 14,
15, 14, 13,
// top 24
16, 17, 18,
19, 18, 17,
// bottom 30
20, 21, 22,
23, 22, 21
]), 1));
bg.clearGroups();
// start, count, material index
bg.addGroup(0, 6, 0);
bg.addGroup(6, 6, 1);
bg.addGroup(12, 6, 2);
bg.addGroup(18, 6, 3);
bg.addGroup(24, 6, 4);
bg.addGroup(30, 6, 5);
var materials = [
new THREE.MeshLambertMaterial({color:"red"}),
new THREE.MeshLambertMaterial({color:"green"}),
new THREE.MeshLambertMaterial({color:"blue"}),
new THREE.MeshLambertMaterial({color:"cyan"}),
new THREE.MeshLambertMaterial({color:"magenta"}),
new THREE.MeshLambertMaterial({color:"yellow"})
];
mesh = new THREE.Mesh(bg, materials);
mesh.scale.set(5, 5, 5);
scene.add(mesh);
}
function init() {
document.body.style.backgroundColor = "slateGray";
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
document.body.style.overflow = "hidden";
document.body.style.margin = "0";
document.body.style.padding = "0";
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 50;
scene.add(camera);
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.dynamicDampingFactor = 0.5;
controls.rotateSpeed = 3;
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
resize();
window.onresize = resize;
populateScene();
animate();
}
function resize() {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
if (renderer && camera && controls) {
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
controls.handleResize();
}
}
function render() {
renderer.render(scene, camera);
}
function animate() {
mesh.rotation.x += 0.015;
mesh.rotation.y += 0.017;
mesh.rotation.z += 0.019;
requestAnimationFrame(animate);
render();
controls.update();
stats.update();
}
function threeReady() {
init();
}
(function() {
function addScript(url, callback) {
callback = callback || function() {};
var script = document.createElement("script");
script.addEventListener("load", callback);
script.setAttribute("src", url);
document.head.appendChild(script);
}
addScript("https://threejs.org/build/three.js", function() {
addScript("https://threejs.org/examples/js/controls/TrackballControls.js", function() {
addScript("https://threejs.org/examples/js/libs/stats.min.js", function() {
threeReady();
})
})
})
})();
Edit: Adding a second example using the base BoxBufferGeometry
Based on pailhead's comment to the original post, here's a snippet which uses unmodified BoxBufferGeometry. But as they mentioned in their comment, you'll still need to know which group corresponds to which face.
var renderer, scene, camera, controls, stats, mesh;
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 35,
NEAR = 1,
FAR = 1000;
function populateScene() {
var bg = new THREE.BoxBufferGeometry(1, 1, 1);
var materials = [
new THREE.MeshLambertMaterial({color:"red"}),
new THREE.MeshLambertMaterial({color:"green"}),
new THREE.MeshLambertMaterial({color:"blue"}),
new THREE.MeshLambertMaterial({color:"cyan"}),
new THREE.MeshLambertMaterial({color:"magenta"}),
new THREE.MeshLambertMaterial({color:"yellow"})
];
mesh = new THREE.Mesh(bg, materials);
mesh.scale.set(10, 10, 10);
scene.add(mesh);
}
function init() {
document.body.style.backgroundColor = "slateGray";
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
document.body.style.overflow = "hidden";
document.body.style.margin = "0";
document.body.style.padding = "0";
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 50;
scene.add(camera);
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.dynamicDampingFactor = 0.5;
controls.rotateSpeed = 3;
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
resize();
window.onresize = resize;
populateScene();
animate();
}
function resize() {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
if (renderer && camera && controls) {
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
controls.handleResize();
}
}
function render() {
renderer.render(scene, camera);
}
function animate() {
mesh.rotation.x += 0.015;
mesh.rotation.y += 0.017;
mesh.rotation.z += 0.019;
requestAnimationFrame(animate);
render();
controls.update();
stats.update();
}
function threeReady() {
init();
}
(function() {
function addScript(url, callback) {
callback = callback || function() {};
var script = document.createElement("script");
script.addEventListener("load", callback);
script.setAttribute("src", url);
document.head.appendChild(script);
}
addScript("https://threejs.org/build/three.js", function() {
addScript("https://threejs.org/examples/js/controls/TrackballControls.js", function() {
addScript("https://threejs.org/examples/js/libs/stats.min.js", function() {
threeReady();
})
})
})
})();
Using groups will split the geometry in 6 faces, for drawing a simple cube you can also use a simple custom ShaderMaterial.
Splitting geometry in 6 groups requires more draw calls, instead of using 1 draw call for drawing a cube you are using 6, one for each face.
Using a ShaderMaterial requires only 1 draw call:
Vertex Shader:
attribute vec3 vertexColor;
varying vec3 vColor;
void main() {
vColor = vertexColor;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}
Fragment Shader:
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.);
}
This way you could also use GLSL color blending for merging different colors.
Custom ShaderMaterial just setting vertex and fragment shader source strings:
const ColorCubeShader = function () {
THREE.ShaderMaterial.call(this, {
vertexShader: vertexShaderSrc,
fragmentShader: fragmentShaderSrc
})
}
ColorCubeShader.prototype = Object.create(THREE.ShaderMaterial.prototype)
ColorCubeShader.prototype.constructor = ColorCubeShader
Color Cube custom Mesh:
/**
* Convenience method for coloring a face
* #param {Number} r
* #param {Number} g
* #param {Number} b
* #returns {Array}
*/
const buildVertexColorArrayFace = function (r, g, b) {
return [
r, g, b,
r, g, b,
r, g, b,
r, g, b
]
}
const ColorCube = function (size) {
const geometry = new THREE.BoxBufferGeometry(size, size, size)
// build color array
let colorArray = []
colorArray = colorArray
.concat(buildVertexColorArrayFace(1, 0, 0))
.concat(buildVertexColorArrayFace(0, 1, 0))
.concat(buildVertexColorArrayFace(0, 0, 1))
.concat(buildVertexColorArrayFace(1, 0, 1))
.concat(buildVertexColorArrayFace(1, 1, 0))
.concat(buildVertexColorArrayFace(0, 1, 1))
// create a buffer attribute for the colors (for attribute vec3 vertexColor)
const colorAttribute = new THREE.Float32BufferAttribute(
new Float32Array(colorArray), 3)
// set attribute vertexColor in vertex shader
geometry.setAttribute('vertexColor', colorAttribute)
// custom Shader Material instance
const material = new ColorCubeShader()
THREE.Mesh.call(this, geometry, material)
}
ColorCube.prototype = Object.create(THREE.Mesh.prototype)
ColorCube.prototype.constructor = ColorCube
Use it:
const cube = new ColorCube(1)
cube.position.set(0, 2, -2)
scene.add(cube)
three.js v53
In three.js v 53 on JSFiddle, making a custom shape, extruding it and applying a texture and wireframe shows the extruded faces being whole, complete squares.
JSFiddle: custom shape extruded in v53 with complete, un-split extrusion faces
three.js v74
The exact same code but three.js v 74 splits the extrude faces into triangles.
JSFiddle: custom shape extruded in v74 with segmented (into triangles) extrusion faces
Question 1: how can I eliminate the segmentation into triangles of the extruded faces in v74?
Bonus Question 2: how can I eliminate the segmentation into triangles on the main face of my shape that I extrude (what I was originally trying to solve when I noticed the extrusion differences between three.js versions)
Both are making my texturing journey harder. Many thanks.
Code for both JSFiddles:
(Some code borrowed from another JS Fiddle but I can't find it again to credit)
/****
Create the texture as I can't use an image on my server
*****/
var canvas = document.getElementById("texture"),
context = canvas.getContext("2d");
canvas.width = 50;
canvas.height = 50;
context.strokeStyle = "#5588ff";
context.lineWidth = 2;
context.moveTo(0, 10);
context.lineTo(50, 10);
context.moveTo(0, 20);
context.lineTo(50, 20);
context.moveTo(0, 30);
context.lineTo(50, 30);
context.moveTo(0, 40);
context.lineTo(50, 40);
context.stroke();
/***********/
var scene, camera, renderer, shape;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, 2, 1, 10000);
camera.position.z = 200;
scene.add(this.camera);
var light = new THREE.AmbientLight(0xffffff);
scene.add(light);
renderer = new THREE.WebGLRenderer({
antialias: true, alpha:true
});
document.getElementById("scene").appendChild(renderer.domElement);
renderer.setSize(document.getElementById("scene").scrollWidth, document.getElementById("scene").scrollHeight);
var points = []
points.push(new THREE.Vector2(100, 0));
points.push(new THREE.Vector2(100, 60));
points.push(new THREE.Vector2(40, 90));
points.push(new THREE.Vector2(-40, 90));
points.push(new THREE.Vector2(-100, 60));
points.push(new THREE.Vector2(-100, 0));
// var path = new THREE.LineCurve3(new THREE.Vector3(45, 0, 0), new THREE.Vector3(-45, 0, 0));
var extrusionSettings = {
steps: 1,
bevelEnabled: false,
amount: 90
};
shape = new THREE.Shape(points);
var geometry = new THREE.ExtrudeGeometry(shape, extrusionSettings),
texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
var material = new THREE.MeshBasicMaterial({
color: 0xFF00FF,
map: texture
});
// *** UVMapping stuff I copied off an example. I don't know what it's doing but the texture won't work without it.
geometry.faceUvs = [
[]
];
geometry.faceVertexUvs = [
[]
];
for (var f = 0; f < geometry.faces.length; f++) {
var faceuv = [
new THREE.Vector2(0, 1),
new THREE.Vector2(1, 1),
new THREE.Vector2(1, 0),
new THREE.Vector2(0, 0)
];
geometry.faceUvs[0].push(new THREE.Vector2(0, 1));
geometry.faceVertexUvs[0].push(faceuv);
}
// add a wireframe to highlight the segmentation (or lack of in this v53 demo)
mesh = THREE.SceneUtils.createMultiMaterialObject(geometry, [material, new THREE.MeshBasicMaterial({
color: 0x000000,
wireframe: true,
transparent: true
})]);
mesh.position.z = -50;
mesh.position.y = -40;
mesh.rotation.y = 0.8;
mesh.rotation.z = 0.4;
scene.add(mesh);
animate = function() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
mesh.rotation.y += 0.02;
mesh.rotation.x += 0.02;
};
animate();
THREE.ExtrudeGeometry creates default UVs for you based on the coordinates of the extruded shape. (Look at the geometry of the extruded shape and inspect the UVs that are computed for you.)
The auto-generated UVs will likely be outside the normal range of [ 0, 1 ]. You can accommodate that by using a pattern like so:
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.offset.set( 0, 0.5 );
texture.repeat.set( 0.01, 0.01 );
Updated fiddle: http://jsfiddle.net/vxr3Ljf3/4/
Alternatively, you can specify your own custom UV generator like so:
var extrusionSettings = {
steps: 1,
bevelEnabled: false,
amount: 90,
UVGenerator: myUVGenerator
};
Base your custom UV generator on THREE.ExtrudeGeometry.WorldUVGenerator.
three.js r.74