Tileable (repeating) regions of textures? - three.js

Is it possible to specify rectangular regions of a texture that should repeat across single triangles/quads?
If not, I think I know how I'd write the shader from scratch. But is it possible to add the uniforms, attributes and GLSL code bits to any of the existing three.js materials? (i.e., so I can still use phong, lambert, etc. and apply my edits.)

You want to repeat a sub-region of a texture, but three.js allows for repeating the entire texture.
There is a work-around, however. You will need to tessellate your geometry, and modify its UVs.
Here is how you can do that with PlaneGeometry.
// geometry
var geometry = new THREE.PlaneGeometry( 10, 10, 4, 4 ); // set the pattern repeat here: it is 4
// reset the uvs to be 0 or 1 for each face
var uvs = geometry.faceVertexUvs[ 0 ];
for( i = 0; i < uvs.length; i += 2 ) {
uvs[ i ][ 0 ].set( 0, 1 ); // first triangle
uvs[ i ][ 1 ].set( 0, 0 );
uvs[ i ][ 2 ].set( 1, 1 );
uvs[ i + 1 ][ 0 ].set( 0, 0 ); // second triangle
uvs[ i + 1 ][ 1 ].set( 1, 0 );
uvs[ i + 1 ][ 2 ].set( 1, 1 );
}
// texture
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.offset.set( 0.5, 0.25 ); // set the sub-texture offset here
texture.repeat.set( 0.25, 0.25 ); // set the sub-texture scale here
If you do not like that solution, you can extend one of the three.js built-in materials using the approach described in this SO post.
three.js r.69

Related

How to make a not squared texture fit in a "background-size:cover" way to a geometry plane in Three.js?

I want my texture to have the same behaviour than the "background-size:cover" css property.
I'd like to work with uvs coordinates.
I looked at this answer and start to work on a solution : Three.js Efficiently Mapping Uvs to Plane
I try to have the same dimension/position planes that some div of my DOM.
This is what I want :
And this is the result I get with this code : the dimension and position are good, the ratio of my texture looks good too but it seems like there's a scale issue :
let w = domElmt.clientWidth / window.innerHeight;
let h = domElmt.clientHeight / window.innerHeight;
geometry = new THREE.PlaneGeometry(w, h);
var uvs = geometry.faceVertexUvs[ 0 ];
uvs[ 0 ][ 0 ].set( 0, h );
uvs[ 0 ][ 1 ].set( 0, 0 );
uvs[ 0 ][ 2 ].set( w, h );
uvs[ 1 ][ 0 ].set( 0, 0 );
uvs[ 1 ][ 1 ].set( w, 0 );
uvs[ 1 ][ 2 ].set( w, h );
tex = new THREE.TextureLoader().load('image.jpg'));
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
material = new THREE.MeshBasicMaterial( { map: tex } );
mesh = new THREE.Mesh( geometry, material );
Should I play with the repeat attribute of my texture or can I fully made this behaviour using uvs ? Thank you
https://en.wikipedia.org/wiki/UV_mapping
UV mapping values range from 0 to 1, inclusive, and represent a percentage mapping across your texture image.
You're using a ratio of the div size vs the window size, which is likely much smaller than 1, and would result in the "zoomed in" effect you're seeing.
For example, if your w and h result in the value 0.5, then The furthest top-right corner of the mapped texture will be the exact center of the image.
background-style: cover:
Scales the image as large as possible without stretching the image. If the proportions of the image differ from the element, it is cropped either vertically or horizontally so that no empty space remains.
In other words, it will scale the image based on the size of the short side, and crop the rest. So let's assume you have a nice 128x512 image, and a 64x64 space. cover would scale the width of 128 down to 64 (a scale factor of 0.5), so multiply 512 by 0.5 to get the new height (128). Now your w would still be 1, but your h will be 128 / 512 = 0.25. Your texture will now fit to the width, and crop the height.
You'll need to perform this calculation for each image-to-container size relationship to find the proper UVs, keeping in mind that the scaling is always relevant to the short side.
You don't need to generate UVs, you can just use texture.repeat and texture.offset
const aspectOfPlane = planeWidth / planeHeight;
const aspectOfImage = image.width / image.height;
let yScale = 1;
let xScale = aspectOfPlane / aspectOfImage;
if (xScale > 1) { // it doesn't cover so based on x instead
xScale = 1;
yScale = aspectOfImage / aspectOfPlane;
}
texture.repeat.set(xScale, yScale);
texture.offset.set((1 - xScale) / 2, (1 - yScale) / 2);
'use strict';
/* global THREE */
async function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 50;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 4;
const scene = new THREE.Scene();
const loader = new THREE.TextureLoader();
function loadTexture(url) {
return new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject);
});
}
const textures = await Promise.all([
"https://i.imgur.com/AyOufBk.jpg",
"https://i.imgur.com/ZKMnXce.png",
"https://i.imgur.com/TSiyiJv.jpg",
"https://i.imgur.com/v38pV.jpg",
].map(loadTexture));
const geometry = new THREE.PlaneBufferGeometry(1, 1);
const material = new THREE.MeshBasicMaterial({map: textures[0]});
const planeMesh = new THREE.Mesh(geometry, material);
scene.add(planeMesh);
let texIndex = 0;
function setTexture() {
const texture = textures[texIndex];
texIndex = (texIndex + 1) % textures.length;
// pick and random width and height for plane
const planeWidth = rand(1, 4);
const planeHeight = rand(1, 4);
planeMesh.scale.set(planeWidth, planeHeight, 1);
const image = texture.image;
const aspectOfPlane = planeWidth / planeHeight;
const aspectOfImage = image.width / image.height;
let yScale = 1;
let xScale = aspectOfPlane / aspectOfImage;
if (xScale > 1) { // it doesn't cover so based on x instead
xScale = 1;
yScale = aspectOfImage / aspectOfPlane;
}
texture.repeat.set(xScale, yScale);
texture.offset.set((1 - xScale) / 2, (1 - yScale) / 2);
material.map = texture;
}
setTexture();
setInterval(setTexture, 1000);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r105/three.min.js"></script>
texture.repeat and texture.offset are really just applied to the UVs so if you really want UVs it's
newU = u * repeat.x + offset.x;
newV = v * repeat.y + offset.y;
so using the code above
offsetX = (1 - xScale) / 2;
offsetY = (1 - yScale) / 2;
u0 = offsetX;
v0 = offsetY;
u1 = offsetX + xScale;
v1 = offsetY + yScale;
so
var uvs = geometry.faceVertexUvs[ 0 ];
uvs[ 0 ][ 0 ].set( u0, v1 );
uvs[ 0 ][ 1 ].set( u0, v0 );
uvs[ 0 ][ 2 ].set( u1, v1 );
uvs[ 1 ][ 0 ].set( u0, v0 );
uvs[ 1 ][ 1 ].set( u1, v0 );
uvs[ 1 ][ 2 ].set( u1, v1 );

Three.js self transparency with intersecting polygons

I'm trying to make a simple tree billboard of two crossed planes with a partially transparent texture. The self-transparency only works for one plane, I'm assuming because of depth-sorting problems when geometry intersects.
See here: https://jsfiddle.net/2q5a7fzy/21/
The geometry is fairly simple:
geometry.vertices.push(
new THREE.Vector3( -1, -1, 0 ),
new THREE.Vector3( -1, 1, 0 ),
new THREE.Vector3( 1, 1, 0 ),
new THREE.Vector3( 1, -1, 0 ),
new THREE.Vector3( 0, -1, -1 ),
new THREE.Vector3( 0, 1, -1 ),
new THREE.Vector3( 0, 1, 1 ),
new THREE.Vector3( 0, -1, 1 ) );
geometry.faces.push(
new THREE.Face3( 0, 1, 2 ),
new THREE.Face3( 0, 2, 3 ),
new THREE.Face3( 4, 5, 6 ),
new THREE.Face3( 4, 6, 7 ) );
I don't want to use the PointCloud billboards because I'd like the trees to be upright even when the camera is above them, rather than always camera-facing.
Anyone have a possible workaround? Can I sort the individual polygons before rendering, somehow? Are there other ways to do billboards that rotate on a fixed axis?
If you have overlapping, textured objects with transparent regions, then one solution to artifacts caused by depth sorting is to set the alphaTest property of the object's material:
material.alphaTest = 0.5; // between 0 and 1
updated fiddle: https://jsfiddle.net/c5qbd8rm/
three.js r.71

Three.js Efficiently Mapping Uvs to Plane

I'm working on a game where there are many walls of the same texture but at different lengths/heights. Currently I am cloning a base texture and setting the repeat values for each wall. This creates to many textures in memory.
What I want to do is to alter the planes uvs to fit the textures properly. I took a stab at this a came up with some meh results.
var X = 2000;
var Y = 1000;
var seg = Math.ceil(X/Y);
var wallgeo = new THREE.PlaneGeometry(X,Y,seg,1);
var uvs = [];
for(var i=0,len=wallgeo.faces.length;i<len;i+=2){
uvs.push([new THREE.Vector2(0,1),new THREE.Vector2(0,0),new THREE.Vector2(1,1)]);
uvs.push([new THREE.Vector2(0,0),new THREE.Vector2(1,0),new THREE.Vector2(1,1)]);
}
wallgeo.faceVertexUvs = [uvs];
wallgeo.uvsNeedUpdate = true;
var walltex = DDSLoader.load('materialmaptextures/21_2048.dds');
var wallmat = new THREE.MeshPhongMaterial({map:walltex, transparent:true, wireframe:false});
wall = new THREE.Mesh(wallgeo,wallmat);
wall.position.set(0,Y/2,-300);
scene.add(wall);
The problems with this as you can probably tell are that for one additional faces are created, not a huge deal but I would really like to avoid this. Most importantly if the planes length is not evenly divisible by its height the texture will stretch/compress. Also, the planes height segments in this are fixed causing restrictions.
I'm not so much concerned with creating additional faces. Is this necessary? Can one face have more than one uv per triangle? Does it need to?
I am new to messing with Uv's, there has got to be an efficient way to make my walls not look absurd without blowing up memory, right?
Demo: http://www.titansoftime.com/uv.php
You want to be able to modify the UVs of a PlaneGeometry so a repeated texture always renders the same size, regardless of the dimensions of the plane. This is an alternative to setting texture.repeat values.
Here is the pattern to follow:
// geometry
var width = 20; // width of plane in world units
var height = 10; // height of plane in world units
var size = 5; // texture block size in world units
var w = width / size;
var h = height / size;
var geometry = new THREE.PlaneGeometry( width, height, 1, 1 );
var uvs = geometry.faceVertexUvs[ 0 ];
uvs[ 0 ][ 0 ].set( 0, h );
uvs[ 0 ][ 1 ].set( 0, 0 );
uvs[ 0 ][ 2 ].set( w, h );
uvs[ 1 ][ 0 ].set( 0, 0 );
uvs[ 1 ][ 1 ].set( w, 0 );
uvs[ 1 ][ 2 ].set( w, h );
// texture
var texture = THREE.ImageUtils.loadTexture( "..." );
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
three.js r.69

how can I create an axonometric oblique / cavalier / cabinet with threejs?

I found examples on how to create (axonometric) isometric camera in Isometric camera with THREE.js, but how can I create an axonometric oblique?
You can render a scene with an oblique cabinet perspective using a work-around in which the camera is an orthographic one, and the geometry of your mesh is skewed with a shear matrix prior to rendering.
// create shear matrix
var alpha = Math.PI / 6; // or Math.PI / 4
var Syx = 0,
Szx = - 0.5 * Math.cos( alpha ),
Sxy = 0,
Szy = - 0.5 * Math.sin( alpha ),
Sxz = 0,
Syz = 0;
var matrix = new THREE.Matrix4();
matrix.set( 1, Syx, Szx, 0,
Sxy, 1, Szy, 0,
Sxz, Syz, 1, 0,
0, 0, 0, 1 );
// apply shear matrix to geometry
mesh.geometry.applyMatrix4( matrix ); // this is the work-around
EDIT: You can alternatively use the method Matrix4.makeShear() to populate a shear matrix.
three.js r.144
Based on #WestLangley's code, here's how you can do an oblique projection without modifying the geometry, just the camera's matrix.
// shear matrix
// ref: http://www.flipcode.com/documents/matrfaq.html#Q43
// | 1 Syx Szx 0 |
// | |
// | Sxy 1 Szy 0 |
// M = | |
// | Sxz Syz 1 0 |
// | |
// | 0 0 0 1 |
// | |
var alpha = Math.PI / 6; // or Math.PI / 4
var Syx = 0,
Szx = - 0.5 * Math.cos( alpha ),
Sxy = 0,
Szy = - 0.5 * Math.sin( alpha ),
Sxz = 0,
Syz = 0;
var matrix = new THREE.Matrix4();
matrix.set( 1, Syx, Szx, 0,
Sxy, 1, Szy, 0,
Sxz, Syz, 1, 0,
0, 0, 0, 1 );
// apply shear matrix to camera
camera.projectionMatrix.multiply(matrix);
camera.projectionMatrixInverse.getInverse( camera.projectionMatrix );
You would make these projectionMatrix modifications after calling camera.updateProjectionMatrix(). I looked inside the code of camera.updateProjectionMatrix to figure out how to do this, including learning of the inverse matrix that it stores.
One slight downside to this is it moves the view somewhat, but you can just move the camera position or frustum boundaries to account for that. (Maybe there's a better way.)
Fiddle
three.js r.120

three.js: multiple color on the same face

I have a cube of size 1 x 1 x 2. On the larger (1 x 2) face, I would like to show one color on half of the face, and another color on the other half. What would the recommended way of implementing this? Should I use hierarchy to build this 1 x 1 x 2 cube using two 1 x 1 x 1 cubes of different face color?
Here is the pattern to follow. Adjust to your liking:
var geometry = new THREE.CubeGeometry( 10, 10, 20, 1, 1, 2 );
for ( var i = 0; i < geometry.faces.length; i ++ ) {
geometry.faces[ i ].color.setHSL( Math.random(), 0.5, 0.5 ); // pick your colors
}
var material = new THREE.MeshBasicMaterial( { vertexColors: THREE.FaceColors } );
var mesh = new THREE.Mesh( geometry, material );
If you are using CanvasRenderer, you can set material.overdraw = 0.5 to try to eliminate the diagonal lines. This is not required for WebGLRenderer.
three.js r.60

Resources