绕枢轴点/轴(Three.js / GLSL)弯曲/弯曲所有顶点

时间:2018-07-30 14:28:01

标签: three.js opengl-es glsl webgl

我正在尝试找出如何围绕特定的枢轴点/轴扭曲Three.js场景中的所有坐标。最好的描述方式是,好像我要在场景中的某个地方放置一个试管,场景中的其他所有内容都将围绕该轴弯曲并保持与该轴相同的距离。

如果有帮助,此图就是我想要实现的。顶部好像是从侧面看场景,而底部好像是从一个角度看场景。红色点/线是枢轴点所在的位置。

Warp around a pivot point

为了使事情更加复杂,我想停止曲线/翘曲自身的回绕,因此,当曲线为水平或垂直时,曲线就停止了,如图中右上角的示例。

是否有任何关于如何使用GLSL着色器实现此目标的见解,理想情况下是在Three.js中,但是如果它们可以用其他方式清楚地描述,我会尝试进行翻译?

我也乐于接受其他方法,因为我不确定如何最好地描述我所追求的。基本上,我想要一个反向的“弯曲世界”效果,其中场景在弯曲并远离您。

Curved world in Unity

1 个答案:

答案 0 :(得分:1)

首先,我将像您的顶视图一样在2D模式下进行操作。

我不知道这是否是正确的方法,或者甚至是一个好方法,但是,以2D方式进行的操作似乎比3D容易,而且您想要的效果实际上是2D。 X根本没有变化,只有Y和Z发生了变化,因此以2D方式求解似乎可以解决。

基本上,我们选择一个圆的半径。对于超出圆心的X的每个单位,在该半径处我们希望将一个水平单位缠绕到圆周围的一个单位。给定半径,我们知道绕圆的距离为2 * PI * radius,因此我们可以轻松计算绕圆旋转多远以获得一个单位。只是1 / circumference * Math.PI * 2,我们会在圆心之外的指定距离内完成

const m4 = twgl.m4;
const v3 = twgl.v3;
const ctx = document.querySelector('canvas').getContext('2d');
const gui = new dat.GUI();

resizeToDisplaySize(ctx.canvas);

const g = {
 rotationPoint: {x: 100, y: ctx.canvas.height / 2 - 50},
 radius: 50,
 range: 60,
};

gui.add(g.rotationPoint, 'x', 0, ctx.canvas.width).onChange(render);
gui.add(g.rotationPoint, 'y', 0, ctx.canvas.height).onChange(render);
gui.add(g, 'radius', 1, 100).onChange(render);
gui.add(g, 'range', 0, 300).onChange(render);

render();
window.addEventListener('resize', render);

function render() {
  resizeToDisplaySize(ctx.canvas);
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  const start = g.rotationPoint.x;
  const curveAmount = g.range / g.radius;
 
  const y = ctx.canvas.height / 2;

  drawDot(ctx, g.rotationPoint.x, g.rotationPoint.y, 'red');
  ctx.beginPath();
  ctx.arc(g.rotationPoint.x, g.rotationPoint.y, g.radius, 0, Math.PI * 2, false);
  ctx.strokeStyle = 'red';
  ctx.stroke();
    
  ctx.fillStyle = 'black';

  const invRange = g.range > 0 ? 1 / g.range : 0;  // so we don't divide by 0
  for (let x = 0; x < ctx.canvas.width; x += 5) {
    for (let yy = 0; yy <= 30; yy += 10) {
      const sign = Math.sign(g.rotationPoint.y - y);
      const amountToApplyCurve = clamp((x - start) * invRange, 0, 1);

      let mat = m4.identity();
      mat = m4.translate(mat, [g.rotationPoint.x, g.rotationPoint.y, 0]);
      mat = m4.rotateZ(mat, curveAmount * amountToApplyCurve * sign);
      mat = m4.translate(mat, [-g.rotationPoint.x, -g.rotationPoint.y, 0]);

      const origP = [x, y + yy, 0];
      origP[0] += -g.range * amountToApplyCurve;
      const newP = m4.transformPoint(mat, origP);
      drawDot(ctx, newP[0], newP[1], 'black');
    }
  }
}

function drawDot(ctx, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x - 1, y - 1, 3, 3);
}

function clamp(v, min, max) {
  return Math.min(max, Math.max(v, min));
}

function resizeToDisplaySize(canvas) {
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width;
    canvas.height = height;
  }
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
<!-- using twgl just for its math library -->
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.2/dat.gui.min.js"></script>

请注意,唯一匹配的位置是半径接触点线时。在半径内,事物将被挤压,在半径外,事物将被拉伸。

将其沿Z方向放置在着色器中以进行实际使用

const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector('canvas'),
});
const gui = new dat.GUI();

const scene = new THREE.Scene();

const fov = 75;
const aspect = 2;  // the canvas default
const zNear = 1;
const zFar = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);

function lookSide() {
  camera.position.set(-170, 35, 210);
  camera.lookAt(0, 25, 210);
}

function lookIn() {
  camera.position.set(0, 35, -50);
  camera.lookAt(0, 25, 0);
}

{
  scene.add(new THREE.HemisphereLight(0xaaaaaa, 0x444444, .5));
  const light = new THREE.DirectionalLight(0xffffff, 1);
  light.position.set(-1, 20, 4 - 15);
  scene.add(light);
}

const point = function() {
  const material = new THREE.MeshPhongMaterial({
    color: 'red',
    emissive: 'hsl(0,50%,25%)',
    wireframe: true,
  });
  const radiusTop = 1;
  const radiusBottom = 1;
  const height = 0.001;
  const radialSegments = 32;
  const geo = new THREE.CylinderBufferGeometry(
    radiusTop, radiusBottom, height, radialSegments);
  const sphere = new THREE.Mesh(geo, material);
  sphere.rotation.z = Math.PI * .5;
  const mesh = new THREE.Object3D();
  mesh.add(sphere);
  scene.add(mesh);
  mesh.position.y = 88;
  mesh.position.z = 200;
  return {
    point: mesh,
    rep: sphere,
  };
}();

const vs = `
  // -------------------------------------- [ VS ] ---
  #define PI radians(180.0)
  uniform mat4 center;
  uniform mat4 invCenter;
  uniform float range;
  uniform float radius;
  varying vec3 vNormal;

  mat4 rotZ(float angleInRadians) {
      float s = sin(angleInRadians);
      float c = cos(angleInRadians);

      return mat4(
        c,-s, 0, 0,
        s, c, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1);
  }

  mat4 rotX(float angleInRadians) {
      float s = sin(angleInRadians);
      float c = cos(angleInRadians);

      return mat4( 
        1, 0, 0, 0,
        0, c, s, 0,
        0, -s, c, 0,
        0, 0, 0, 1);  
  }

  void main() {
    float curveAmount = range / radius;
    float invRange = range > 0.0 ? 1.0 / range : 0.0;

    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    vec4 point = invCenter * mvPosition;
    float amountToApplyCurve = clamp(point.z * invRange, 0.0, 1.0);
    float s = sign(point.y);
    mat4 mat = rotX(curveAmount * amountToApplyCurve * s);
    point = center * mat * (point + vec4(0, 0, -range * amountToApplyCurve, 0));
    vNormal = mat3(mat) * normalMatrix * normal;
    gl_Position = projectionMatrix * point;
  }
`;

const fs = `
  // -------------------------------------- [ FS ] ---
varying vec3 vNormal;
uniform vec3 color;
void main() {
  vec3 light = vec3( 0.5, 2.2, 1.0 );
  light = normalize( light );
  float dProd = dot( vNormal, light ) * 0.5 + 0.5;
  gl_FragColor = vec4( vec3( dProd ) * vec3( color ), 1.0 );
}

`;

const centerUniforms = {
  radius: { value: 0 },
  range: { value: 0 },
  center: { value: new THREE.Matrix4() },
  invCenter: { value: new THREE.Matrix4() },
};
function addUniforms(uniforms) {
  return Object.assign(uniforms, centerUniforms);
}


{
  const uniforms = addUniforms({
    color: { value: new THREE.Color('hsl(100,50%,50%)') },
  });
  const material = new THREE.ShaderMaterial( {
    uniforms: uniforms,
    vertexShader: vs,
    fragmentShader: fs,
  });
  const planeGeo = new THREE.PlaneBufferGeometry(1000, 1000, 100, 100);
  const mesh = new THREE.Mesh(planeGeo, material);
  mesh.rotation.x = Math.PI * -.5;
  scene.add(mesh);
}
{
  const uniforms = addUniforms({
    color: { value: new THREE.Color('hsl(180,50%,50%)' ) },
  });
  const material = new THREE.ShaderMaterial( {
    uniforms: uniforms,
    vertexShader: vs,
    fragmentShader: fs,
  });
  const boxGeo = new THREE.BoxBufferGeometry(10, 10, 10, 20, 20, 20);
for (let x = -41; x <= 41; x += 2) {
  for (let z = 0; z <= 40; z += 2) {
      const base = new THREE.Object3D();
      const mesh = new THREE.Mesh(boxGeo, material);
      mesh.position.set(0, 5, 0);
      base.position.set(x * 10, 0, z * 10);
      base.scale.y = 1 + Math.random() * 2;
      base.add(mesh);
      scene.add(base);
    }
  }
}

const g = {
 radius: 59,
 range: 60,
 side: true,
};

class DegRadHelper {
  constructor(obj, prop) {
    this.obj = obj;
    this.prop = prop;
  }
  get v() {
    return THREE.Math.radToDeg(this.obj[this.prop]);
  }
  set v(v) {
    this.obj[this.prop] = THREE.Math.degToRad(v);
  }
}

gui.add(point.point.position, 'z', -300, 300).onChange(render);
gui.add(point.point.position, 'y', -150, 300).onChange(render);
gui.add(g, 'radius', 1, 100).onChange(render);
gui.add(g, 'range', 0, 300).onChange(render);
gui.add(g, 'side').onChange(render);
gui.add(new DegRadHelper(point.point.rotation, 'x'), 'v', -180, 180).name('rotX').onChange(render);
gui.add(new DegRadHelper(point.point.rotation, 'y'), 'v', -180, 180).name('rotY').onChange(render);
gui.add(new DegRadHelper(point.point.rotation, 'z'), 'v', -180, 180).name('rotZ').onChange(render);
render();
window.addEventListener('resize', render);

function render() {
  if (resizeToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
  
  if (g.side) {
    lookSide();
  } else {
    lookIn();
  }
  
  camera.updateMatrixWorld();

  point.rep.scale.set(g.radius, g.radius, g.radius);
  point.point.updateMatrixWorld();

  centerUniforms.center.value.multiplyMatrices(
    camera.matrixWorldInverse, point.point.matrixWorld);
  centerUniforms.invCenter.value.getInverse(centerUniforms.center.value);
  centerUniforms.range.value = g.range;
  centerUniforms.radius.value = g.radius;

  renderer.render(scene, camera);
}

function resizeToDisplaySize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needUpdate = canvas.width !== width || canvas.height !== height;
  if (needUpdate) {
    renderer.setSize(width, height, false);
  }
  return needUpdate;
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.2/dat.gui.min.js"></script>

老实说,我有一种更容易遗失的方式,但目前看来,它似乎正在起作用。