How to have different colors/textures on bottom and top side of a Plane?

Hi all, I’ve got a grid of tiles, which have a THREE.DoubleSide material. These tiles flip 180 degrees when clicked, showing the bottom instead of the top.

Something I couldn’t figure out: Is there any way to have a different color or texture for the bottom than the top of a plane?

(I’m using React-Three-Fiber if that makes any difference)

If you are talking about THREE.PlaneBufferGeometry than no, it’s not possible. However, you can try to represent your plane as a thin box (THREE.BoxBufferGeometry). A box allows you do assign a separate material for each side.

1 Like

He could also duplicate the tiles rotated (less triangles)

2 Likes

Thanks, that’s good to know. I managed to get it working by converting to boxes! Not sure how much difference it will make in performance?

Another option is to use .onBeforeCompile() to modify shaders of built-in materials, using gl_FrontFacing, THREE.DoubleSide and a buffer attribute for the additional set of UV coordinates for back faces of THREE.PlaneBufferGeometry().

изображение

5 Likes

Another very clever solution. :+1: :+1: :+1:

For my Inner Geometry (Triangulation) I can use this very well. I had already thought about asking the corresponding question, but first I wanted to have the geometry as far as possible finished.

Maybe that still needs some adjustment?

Thanks! Will try it soon. :slightly_smiling_face:

1 Like

Hurray, it’s working! :slightly_smiling_face:

Shouldn’t this possibility be at the core of three.js?

I have simplified and changed the example very much.

But the essential thing is

shMaterial.onBeforeCompile = shader => { // @author prisoner849 
...
}

2020-02-12_15.10.50

Live here SimpleDoubleSide(gl_FrontFacing)

In the code are my experiments with rotation and reflection of the texture uv_grid_opengl.jpg

<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/how-to-have-different-colors-textures-on-bottom-and-top-side-of-a-plane/12644/5 -->
<!-- simplified, based on @author prisoner849  https://codepen.io/prisoner849/pen/OJVVpZO -->
<head>
  <title> SimpleDoubleSide(gl_FrontFacing) </title>
  <meta charset="utf-8" /> 
</head>
<body> </body>
<script src="../js/three.min.112.js"></script>
<script src="../js/OrbitControls.js"></script>
<script>

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 60, 1, 0.01, 1000 );
camera.position.set( 1, 2, 3 );

const renderer = new THREE.WebGLRenderer({  antialias: true });
const container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement ); 

renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0xdedede, 1 );

const controls = new THREE.OrbitControls(camera, container);

// https://threejs.org/examples/textures/flakes.png
const tex = new THREE.TextureLoader().load( "https://hofk.de/main/threejs/1TEST/textures/green.png" );
// https://threejs.org/examples/textures/uv_grid_opengl.jpg
const texBack = new THREE.TextureLoader().load( "https://hofk.de/main/threejs/1TEST/textures/yellow.png" );

const shMaterial = new THREE.MeshBasicMaterial( { map: tex, side: THREE.DoubleSide } );
const planeUniforms = { backTexture: { value: texBack } };

shMaterial.onBeforeCompile = shader => { // @author prisoner849 
	shader.uniforms.backTexture = planeUniforms.backTexture;
	shader.vertexShader =
		`
	attribute vec2 backUV;
	varying vec2 vBackUV;
	` + shader.vertexShader;
	shader.vertexShader = shader.vertexShader.replace(
		`#include <fog_vertex>`,
		`#include <fog_vertex>
	vBackUV = backUV;
	`
	);
	shader.fragmentShader =
		`
	uniform sampler2D backTexture;
	varying vec2 vBackUV;
	` + shader.fragmentShader;
	shader.fragmentShader = shader.fragmentShader.replace(
		`#include <map_fragment>`,
		`
	#ifdef USE_MAP
	
	vec4 texelColor = gl_FrontFacing ? texture2D( map, vUv ) : texture2D( backTexture, vBackUV );
	texelColor = mapTexelToLinear( texelColor );
	diffuseColor *= texelColor;
	
	#endif
	`
	);
};

const cylGeom = new THREE.CylinderBufferGeometry( 0.5, 0.4, 1, 36, 8, true );

const wireMaterial = new THREE.MeshBasicMaterial( {side: THREE.DoubleSide, color: 0x000000, wireframe: true, transparent: true, opacity: 0.99 } );
const cylinderWire = new THREE.Mesh( cylGeom, wireMaterial );
scene.add( cylinderWire );

const cylinder  = new THREE.Mesh( cylGeom, shMaterial );
scene.add( cylinder );

// using uv_grid_opengl.jpg for backside and a plane geometry:
// planeGeom.setAttribute( "backUV", new THREE.Float32BufferAttribute([ 1,1, 0,1, 1,0, 0,0 ], 2 ) );
// planeGeom.setAttribute( "backUV", new THREE.Float32BufferAttribute([ 0,0, 1,0, 0,1, 1,1 ], 2 ) ); // head over
// planeGeom.setAttribute( "backUV", new THREE.Float32BufferAttribute([ 0,1, 1,1, 0,0, 1,0 ], 2 ) ); // mirrored
// planeGeom.setAttribute( "backUV", new THREE.Float32BufferAttribute([ 1,0, 0,0, 1,1, 0,1 ], 2 ) ); // head over, mirrored
// const plane = new THREE.Mesh( planeGeom, shMaterial );
// scene.add( plane );
 
render();

function render( ) {

	renderer.render( scene, camera );
	requestAnimationFrame( render );
}

</script>
</html>
3 Likes

I have now { backTexture: { value: texBack } } directly into the shader.

shMaterial.onBeforeCompile = shader => { // @author prisoner849 
	shader.uniforms.backTexture = { backTexture: { value: texBack } }.backTexture;

This makes it more compact, because only 2 textures and the material and .onBeforeCompile = shader => have to be specified.

I don’t have much experience with shaders, and I don’t really see through this part. :confused:

Is there any simplification if I only need simple colors like in the example? :thinking:


UPDATE
It also works with transparency. Great!
2020-02-12_18.05.56

And the inner geometry looks much better in two colours.

@hofk
I follow @pailhead’s suggestion: Instancing + selective bloom
So I simply use uniforms by reference.
It makes things simplier.

If I have

var uniforms = {
  time: {value: 0}
}

...

mat.onBeforeCompile = shader => {
  shader.uniforms.time = uniforms.time;
  ...
}

// and then in the animation loop
...
  uniforms.time.value = clock.getElapsedTime();

I don’t need to check in my animation loop, if uniforms.time exists or defined.

Whereas pushing uniforms from .onBeforeCompile() makes things messy/complicated.

var shaderMaterial;

...
mat.onBeforeCompile = shader => {
  shader.uniforms.time = {value: 0};
  ...
  shaderMaterial = shader;
}

// in the animation loop
  if (shaderMaterial) { // without this check you'll get an error message that object is undefined
    shaderMaterial.uniforms.time.value = clock.getElapsedTime();
  }

and you can’t access uniforms from material itself, like so:

// in the animation loop
  mat.uniforms.time.value = clock.getElapsedTime(); // causes error

Also good to use this addon from @Fyrestar : Chainable onBeforeCompile + Uniforms access (per Mesh)

3 Likes

I now have a form that is simple and practical for me. The shader is a black box and can be replaced by another variant if necessary. (not by me :dizzy_face: )

const material1 = new THREE.MeshBasicMaterial( { side: THREE.DoubleSide, color: 0x000000, wireframe: true, transparent: true, opacity: 0.99 } );
const tex = new THREE.TextureLoader().load( "textures/green.png" );
const texBack = new THREE.TextureLoader().load( "textures/yellow.png" );
const obc = shader => { // @author prisoner849 
	shader.uniforms.backTexture = { backTexture: { value: texBack } }.backTexture;
	shader.vertexShader =
		`
	attribute vec2 backUV;
	varying vec2 vBackUV;
	` + shader.vertexShader;
	shader.vertexShader = shader.vertexShader.replace(
		`#include <fog_vertex>`,
		`#include <fog_vertex>
	vBackUV = backUV;
	`
	);
	shader.fragmentShader =
		`
	uniform sampler2D backTexture;
	varying vec2 vBackUV;
	` + shader.fragmentShader;
	shader.fragmentShader = shader.fragmentShader.replace(
		`#include <map_fragment>`,
		`
	#ifdef USE_MAP
	
	vec4 texelColor = gl_FrontFacing ? texture2D( map, vUv ) : texture2D( backTexture, vBackUV );
	texelColor = mapTexelToLinear( texelColor );
	diffuseColor *= texelColor;
	
	#endif
	`
	);
}; // @author prisoner849 
const material2 = new THREE.MeshBasicMaterial( { map: tex, side: THREE.DoubleSide, transparent: true, opacity: 0.9, onBeforeCompile: obc } );

I wanted to use this two-color display in a current project and found that the pen @prisoner849 no longer works. What works is the copy in the Collection of examples from discourse.threejs.org :
DoubleSide(gl_FrontFacing)

However, I get an error when customizing my above example SimpleDoubleSide(gl_FrontFacing) to r160:

THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
. . .

I assume there have been breaking changes to r112?

Seems, I fixed it. :sweat_smile: Works with r161.

2 Likes

Thanks alot! I wouldn’t have had a chance!

What caused the shader to have to be changed?

Now it works perfectly again.

2024-02-06 19.17.46

I didn’t dig that deep. Names of variables have changed. For example, vUv => vMapUv.

2 Likes

If you’re not that deep into shaders, it’s problematic when such name changes make a solution unusable.

I also wanted to try out transparency now. It worked with r112, see picture above.

After adding transparent: true, opacity: 0.9 it is transparent with the side of the front color. :thinking:

Tried it with forceSinglePass: true on a material with transparency, backside colour is on now. :thinking:

1 Like