1

I'm applying a shader as a texture to a plane in an isometric scene. The plane lays flat with x,z dimensions. I'm having trouble getting the shader pattern to match the isometric perspective as the scene it's in.

Here's an example where the shader rotates with the plane (like a regular texture) by passing in the orientation as a uniform.

Here's a "2d" (orthographic) projection of the shader texture:

var TWO_PI = Math.PI * 2;
var PI = Math.PI;

var width = window.innerHeight - 50;
var height = window.innerHeight - 50;
var aspect = width / height;
var planeSize = width * 0.75;

var clock = new THREE.Clock();

var camera, scene, renderer;
var plane, geom_plane, mat_plane;

function init() {

  // ---------- scene 

  scene = new THREE.Scene();

  // ---------- plane

  var plane_w = planeSize;
  var plane_h = planeSize;

  var geom_plane = new THREE.PlaneGeometry(plane_w,
    plane_h,
    0);
  var mat_plane = new THREE.MeshBasicMaterial({
    color: 0xffff00,
    side: THREE.DoubleSide
  });

  var shaderMaterial_plane = new THREE.ShaderMaterial({
    uniforms: {
      u_resolution: {
        value: new THREE.Vector2(planeSize, planeSize)
      },
      u_rotation_x: {
        value: performance.now() * 0.001
      },
      u_rotation_y: {
        value: performance.now() * 0.001
      }
    },
    vertexShader: document.getElementById('vertexshader').textContent,
    fragmentShader: document.getElementById('fragmentshader').textContent,
    blending: THREE.NormalBlending,
    depthTest: true,
    transparent: true
  });

  plane = new THREE.Mesh(geom_plane, shaderMaterial_plane);
  scene.add(plane);

  // ---------- cam

  camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 1, 5000);
  camera.position.set(0, 0, planeSize);
  camera.lookAt(scene.position);

  // ---------- renderer

  renderer = new THREE.WebGLRenderer({
    antialias: false,
    alpha: true
  });
  renderer.setSize(width, height);
  renderer.setClearColor(0x000000);
  document.body.appendChild(renderer.domElement);
}

function animate() {
  requestAnimationFrame(animate);
  var time = performance.now() * 0.001;

  plane.material.uniforms.u_rotation_x.value = Math.sin(time * 0.2);
  plane.material.uniforms.u_rotation_y.value = Math.cos(time * 0.2);

  var delta = clock.getDelta();
  render();
}

function render() {
  renderer.render(scene, camera);
}

init();
animate();
<script type="x-shader/x-vertex" id="vertexshader">
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">

    uniform vec2 u_resolution;  // Canvas size (width,height)
    uniform float u_rotation_x;
    uniform float u_rotation_y;

    mat2 rotate2d(vec2 _angles){
        return mat2(_angles.x,
                    -_angles.x,
                    _angles.y,
                    _angles.y);
    }

    float map(float value, float min1, float max1, float min2, float max2) {
        return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
    }

    void main() {
        vec2 st = gl_FragCoord.xy/u_resolution.xy;
        vec3 color = vec3(1.0,1.0,1.0);
        float gradientLength = 0.2;
        float t = 18.;

        // move matrix in order to set rotation pivot point to center
        st -= vec2(0.5);

        // rotate
        vec2 u_rotation = vec2(u_rotation_x, u_rotation_y);
        st = rotate2d(u_rotation) * st;

        // move matrix back
        st += vec2(0.5);

        // apply gradient pattern
        vec2 p = vec2(floor(gl_FragCoord.x), floor(gl_FragCoord.y));
        float pp = clamp(gl_FragCoord.y,-0.5,st.y);
        float val = mod((pp + t), gradientLength);
        float alpha = map(val, 0.0, gradientLength, 1.0, 0.0);


        gl_FragColor = vec4(color,alpha);
    }
</script>
<div id="threejs_canvas"></div>
<script src="https://threejs.org/build/three.min.js"></script>

And here it is on the plane in isometric space (with the same rotation):

var TWO_PI = Math.PI * 2;
var PI = Math.PI;

var width = window.innerHeight - 50;
var height = window.innerHeight - 50;
var aspect = width / height;
var canvasCubeSize = width;

var clock = new THREE.Clock();

var camera, scene, renderer;
var wire_cube;
var plane, geom_plane, mat_plane;

function init() {

    // ---------- scene 

    scene = new THREE.Scene();

    // ---------- wire cube 

    var wire_geometry = new THREE.BoxGeometry(canvasCubeSize / 2, canvasCubeSize / 2, canvasCubeSize / 2);
    var wire_material = new THREE.MeshBasicMaterial({
        wireframe: true,
        color: 0xff0000
    });

    wire_cube = new THREE.Mesh(wire_geometry, wire_material);
    scene.add(wire_cube);

    // ---------- plane

    var plane_w = canvasCubeSize / 2;
    var plane_h = plane_w;

    var geom_plane = new THREE.PlaneGeometry(plane_w,
        plane_h,
        0);
    var mat_plane = new THREE.MeshBasicMaterial({
        color: 0xffff00,
        side: THREE.DoubleSide
    });

    var shaderMaterial_plane = new THREE.ShaderMaterial({
        uniforms: {
            u_time: {
                value: 1.0
            },
            u_resolution: {
                value: new THREE.Vector2(canvasCubeSize, canvasCubeSize)
            },
            u_rotation_x: {
                value: wire_cube.rotation.y
            },
            u_rotation_y: {
                value: wire_cube.rotation.y
            }
        },
        vertexShader: document.getElementById('vertexshader').textContent,
        fragmentShader: document.getElementById('fragmentshader').textContent,
        blending: THREE.NormalBlending,
        depthTest: true,
        transparent: true
    });

    plane = new THREE.Mesh(geom_plane, shaderMaterial_plane);
    plane.rotation.x = -PI / 2;
    wire_cube.add(plane);

    // ---------- cam

    camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 1, 5000);
    camera.position.set(canvasCubeSize, canvasCubeSize, canvasCubeSize);
    camera.lookAt(scene.position);

    // ---------- renderer 
    renderer = new THREE.WebGLRenderer({
        antialias: false,
        alpha: true
    });
    renderer.setSize(width, height);
    renderer.setClearColor(0x000000);
    document.body.appendChild(renderer.domElement);
}

function animate() {
    
    requestAnimationFrame(animate);

    var time = performance.now() * 0.001;
    wire_cube.rotation.y = time * 0.2;
    if (wire_cube.rotation.y >= TWO_PI) {
        wire_cube.rotation.y -= TWO_PI;
    }

    plane.material.uniforms.u_time.value = time * 0.005;
    plane.material.uniforms.u_rotation_x.value = Math.sin(wire_cube.rotation.y);
    plane.material.uniforms.u_rotation_y.value = Math.cos(wire_cube.rotation.y);

    var delta = clock.getDelta();
    render();
}

function render() {
    renderer.render(scene, camera);
}

init();
animate();
<script type="x-shader/x-vertex" id="vertexshader">
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
</script>
<script type="x-shader/x-fragment" id="fragmentshader">

      uniform vec2 u_resolution;  // Canvas size (width,height)
      uniform float u_rotation_x;
      uniform float u_rotation_y;

      mat2 rotate2d(vec2 _angles){
          return mat2(_angles.x,
                      -_angles.x,
                      _angles.y,
                      _angles.y);
      }

      float map(float value, float min1, float max1, float min2, float max2) {
          return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
      }

      void main() {
          vec2 st = gl_FragCoord.xy/u_resolution.xy;
          vec3 color = vec3(1.0,1.0,1.0);
          float gradientLength = 0.2;
          float t = 18.;

          // move matrix in order to set rotation pivot point to center
          st -= vec2(0.5);

          // rotate
          vec2 u_rotation = vec2(u_rotation_x, u_rotation_y);
          st = rotate2d(u_rotation) * st;

          // move matrix back
          st += vec2(0.5);

          // apply gradient pattern
          vec2 p = vec2(floor(gl_FragCoord.x), floor(gl_FragCoord.y));
          float pp = clamp(gl_FragCoord.y,-0.5,st.y);
          float val = mod((pp + t), gradientLength);
          float alpha = map(val, 0.0, gradientLength, 1.0, 0.0);


          gl_FragColor = vec4(color,alpha);
      }
  </script>
<div id="threejs_canvas">
</div>
<script src="https://threejs.org/build/three.min.js"></script>

if snippet output is too small see here

The rotation illustrates how the shader isn't mimicking isometric perspective. Notice how the shader pattern doesn't stay fixed relative to the plane's corners as they rotate.

Here's the frag shader:

uniform vec2 u_resolution;  // canvas size (width,height)
    uniform float u_rotation_x;
    uniform float u_rotation_y;

    mat2 rotate2d(vec2 _angles){
        return mat2(_angles.x,
                    -_angles.x,
                    _angles.y,
                    _angles.y);
    }

    float map(float value, float min1, float max1, float min2, float max2) {
        return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
    }

    void main() {
        vec2 st = gl_FragCoord.xy/u_resolution.xy;
        vec3 color = vec3(1.0,1.0,1.0);
        float gradientLength = 0.2;
        float t = 18.;

        // move matrix in order to set rotation pivot point to center
        st -= vec2(0.5);

        // rotate
        vec2 u_rotation = vec2(u_rotation_x, u_rotation_y);
        st = rotate2d(u_rotation) * st;

        // move matrix back
        st += vec2(0.5);

        // apply gradient pattern
        vec2 p = vec2(floor(gl_FragCoord.x), floor(gl_FragCoord.y));
        float pp = clamp(gl_FragCoord.y,-0.5,st.y);
        float val = mod((pp + t), gradientLength);
        float alpha = map(val, 0.0, gradientLength, 1.0, 0.0);


        gl_FragColor = vec4(color,alpha);
    }

Could someone help me understand how to "warp" the matrix in the shader so that when it rotates, it mimics the rotation of a flat plane in isometric space?


Edit: I'm wondering if warping the matrix and applying accurate rotation should be broken up into two separate issues? I'm playing around with changing the rotation speed based on 0 to TWO_PI orientation but maybe that's a solution specific to this example...

A__
  • 1,616
  • 2
  • 19
  • 33

2 Answers2

1

Very interesting problem (+1 for that). How about converting unit circle to ellipse and using 90 degree offseted basis vectors inscribed in it?

ellipse inscribed basis vectors

Ignoring matrix math here GL/GLSL/C++ example:

CPU side draw:

// GLSL Isometric view
float pan[2]={0.5,0.5};
float u[2]={1.0,0.0};
float v[2]={0.5,0.5};
const float deg=M_PI/180.0;
const float da=1.0*deg;;
static float a=0.0;

u[0]=1.0*cos(a);
u[1]=0.5*sin(a);
v[0]=1.0*cos(a+90.0*deg);
v[1]=0.5*sin(a+90.0*deg);
a+=da; if (a>=2.0*M_PI) a-=2.0*M_PI;

glUseProgram(prog_id);
id=glGetUniformLocation(prog_id,"zoom"); glUniform1f(id,0.5);
id=glGetUniformLocation(prog_id,"pan"); glUniform2fv(id,1,pan);
id=glGetUniformLocation(prog_id,"u"); glUniform2fv(id,1,u);
id=glGetUniformLocation(prog_id,"v"); glUniform2fv(id,1,v);

glBegin(GL_QUADS);
glColor3f(1,1,1);
float x=0.0,y=0.0;
glVertex2f(x+0.0,y+0.0);
glVertex2f(x+0.0,y+1.0);
glVertex2f(x+1.0,y+1.0);
glVertex2f(x+1.0,y+0.0);
glEnd();
glUseProgram(0);

Vertex:

#version 120
// Vertex
uniform vec2 pan=vec2(0.5,0.5); // origin [grid cells]
uniform float zoom=0.5;         // scale
uniform vec2 u=vec2(1.0,0.0);   // basis vectors
uniform vec2 v=vec2(0.5,0.5);
varying vec2 pos;               // position [grid cells]
void main()
    {
    pos=gl_Vertex.xy;
    vec2 a=zoom*(gl_Vertex.xy-pan);
    gl_Position=vec4((u*a.x)+(v*a.y),0.0,1.0);
    }

Fragment:

#version 120
// Fragment
varying vec2 pos;               // texture coordinate

void main()
    {
    float a;
    a=2.0*(pos.x+pos.y);
    a-=floor(a);
    gl_FragColor=vec4(a,a,a,1.0);
    }

And finally preview:

preview

The important stuff is in the Vertex shader. So simply use u,v basis vectors to convert from world 2D to Isometric 2D position simply by formula:

isometric = world.x*u + world.y*v

The rest is just pan and zoom ...

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • Hey, thank you so much for your response. I'm having trouble porting this logic to my given example, wondering if you could take a look: https://jsfiddle.net/cmj8bauL/5/. I'm not sure what `pan` and `zoom` are in this context (or what `pan` is in isometric space altogether)... – A__ May 27 '19 at 01:29
  • @A__ Your vertex contains matrices that is wrong. You got `pan,zoom,u,v` instead of all matrices. But if you still want matrices you can [construct one](https://stackoverflow.com/a/28084380/2521214) that matches mine equation Vertex shader. In the Fragment you got some kind of rotation? I do not think that is needed as you rotate in Vertex simply by using the `u,v` basis vectors (as they are already rotated). If you want also z axis you should add grid `z` into isometric position `y` axis ... – Spektre May 27 '19 at 06:26
  • @A__ search `v[0] = 1.0 * Math.cos(a * deg); // why +90?` it should be `v[0] = 1.0 * Math.cos(a + 90.0*deg);` and the next line too. `90 deg` because you want your XY plane basis vectors `u,v` to be 90 deg apart each-other. The "isometric" 3D ilusion is done by the ellipse so x has 1.0 radius and y has 0.5 radius. `pan` is position in grid cells that is centered on screen (so you can move if map is bigger than screen) and `zoom` adjusts size of the cells rendered as GL screen without matrices is in range <-1,+1> ... – Spektre May 27 '19 at 06:31
  • @A__ if I see it right you are not incrementing the animation angle rather scaling time instead. That is OK but the limitation of angle range is then wrong and should be changed to something like this: `wire_cube.rotation.y = time * 0.2 * TWO_PI; wire_cube.rotation.y-=floor(wire_cube.rotation.y); wire_cube.rotation.y/=TWO_PI;` as scaled time can lead to any multiple of `TWO_PI` range ... – Spektre May 27 '19 at 06:42
  • What do you mean by "instead of all matrices"? Are you saying those are all not matrices but they should be? I don't think I understand the wording of that sentence. "But if you still want matrices..."— I suppose not if there's an easier approach! I feel like I'm getting hung up on details of your wording that you maybe didn't intend to be taken literally, but I can't tell. I feel more lost than before. Could you possibly explain given my original example, maybe with pseudocode or a new jsfiddle? – A__ May 27 '19 at 18:37
  • Ok from what I understand you are suggesting the following pipeline: 1) Warp & rotate uv coords in GL app, 2) Receive uv coords as uniforms and apply to `gl_Position` in vert shader 3) Receive pixel `pos` in frag shader & set color – A__ May 27 '19 at 19:09
  • Some specific questions: 1) What is a "grid cell"? 2) How can you and why would you initialize the uniforms with values as you declare them in the vertex shader (i.e. `uniform float zoom=0.5;`) ? Will that overwrite their values being passed in? – A__ May 27 '19 at 19:14
  • @A__ yep the pipeline is as you guessed . If you look at my Vertex I got only `pan,zoom` and `u,v` applied to position if you look at yours `gl_Position = projectionMatrix * modelViewMatrix * vec4((u*position.x)+(v*position.y),0.0, 1.0);` You are using 2 matrices which will mess things up. Yes you can pack all my equations in my Vertex shader into single matrix but I wanted to be as simple and straightforward as it can be... – Spektre May 27 '19 at 20:03
  • @A__ Grid cell is a single isometric tile on the map... Isometric stuff is usually done/rendered on grid maps like here: [Improving performance of click detection on a staggered column isometric grid](https://stackoverflow.com/a/35917976/2521214) or here [How to procedurally generate a Zelda like maze in java](https://stackoverflow.com/a/36263999/2521214) just look at the images ... If you do not have a map and have a singular objects with arbitrary positions instead then use that ... in such case grid cell size is the size of your object – Spektre May 27 '19 at 20:06
0

The solution turns out to be pretty simple. I discovered my question is a dupe, the original containing an example (also explained below) that illustrates the solution.

In my original code I'm getting the pixel xy position using vec2 st = gl_FragCoord.xy/u_resolution.xy; which is the global window position. Getting the relative uv position in the frag shader requires passing in the width and height of the uv surface into the vertex shader in order to get a normalized pixel position using the threejs predefined vec3 position:

uniform float width;
uniform float height;
varying float x;
varying float y;
void main() {
    // Get normalized position
    x = position.x / width;
    y = position.y / height;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

Then they can be used in the frag shader:

varying float x; // -0.5 to 0.5
varying float y; // -0.5 to 0.5
void main() {
    gl_FragColor = vec4(x, y, 0.0, 1.0);
}
A__
  • 1,616
  • 2
  • 19
  • 33