A__
A__

Reputation: 1751

How can I warp a shader matrix to match isometric perspective in a 3d scene?

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...

Upvotes: 1

Views: 957

Answers (2)

A__
A__

Reputation: 1751

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);
}

Upvotes: 0

Spektre
Spektre

Reputation: 51893

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 ...

Upvotes: 1

Related Questions