尝试通过Three.js模拟3D效果

问题描述

我正在尝试实现类似于这种惊人效果效果 https://www.cobosrl.co/

这是我到目前为止的内容 https://codepen.io/routsou/pen/ZEGWJgR?editors=0010

/*--------------------
Setup
--------------------*/
console.clear();
const canvas = document.querySelector('#bubble');

//wobble
let mouseDown = false;
let howMuch = 0;
let howMuchLimit = 0.25;

//ripple
let rippleAmount = 0;
let rippleRatio = 5;
let step = 0;
let sphereVerticesArray = [];
let sphereVerticesnormArray = [];

//raycaster
let raycaster;
let INTERSECTED = null;

let width = canvas.offsetWidth,height = canvas.offsetHeight;
const renderer = new THREE.Webglrenderer({
  canvas: canvas,antialias: true,alpha: true
});
const scene = new THREE.Scene();

const setup = () => {
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize(width,height);
  renderer.setClearColor(0xebebeb,0);
  renderer.shadowMap.enabled = true;
  renderer.shadowMapSoft = true;

  scene.fog = new THREE.Fog(0x000000,10,950);

  const aspectRatio = width / height;
  const fieldOfView = 100;
  const nearPlane = 0.1;
  const farPlane = 10000;
  camera = new THREE.PerspectiveCamera(
    fieldOfView,aspectRatio,nearPlane,farPlane
  );
  
  raycaster = new THREE.Raycaster();
   
  camera.position.x = 0;
  camera.position.y = 0;
  camera.position.z = 300;
}
setup();


/*--------------------
Lights
--------------------*/
let hemispshereLight,shadowLight,light2;
const createLights = () => {
    hemisphereLight = new THREE.HemisphereLight(0xffffff,0x000000,.5)
  
    shadowLight = new THREE.DirectionalLight(0x666666,.4);
    shadowLight.position.set(0,450,350);
    shadowLight.castShadow = true;

    shadowLight.shadow.camera.left = -650;
    shadowLight.shadow.camera.right = 650;
    shadowLight.shadow.camera.top = 650;
    shadowLight.shadow.camera.bottom = -650;
    shadowLight.shadow.camera.near = 1;
    shadowLight.shadow.camera.far = 1000;

    shadowLight.shadow.mapSize.width = 4096;
    shadowLight.shadow.mapSize.height = 4096;
  
  light2 = new THREE.DirectionalLight(0x666666,.25);
    light2.position.set(-600,350,350);
  
  light3 = new THREE.DirectionalLight(0x666666,.15);
    light3.position.set(0,-250,300);

    scene.add(hemisphereLight);  
    scene.add(shadowLight);
  scene.add(light2);
  scene.add(light3);
}
createLights();


/*--------------------
Bubble
--------------------*/
const vertex = width > 575 ? 80 : 40;
const bubbleGeometry = new THREE.SphereGeometry( 150,vertex,vertex );

let bubble;
const createBubble = () => {
  for(let i = 0; i < bubbleGeometry.vertices.length; i++) {
    let vector = bubbleGeometry.vertices[i];
    vector.original = vector.clone();  
  }
  
  const bubbleMaterial = new THREE.MeshStandardMaterial({
    emissive: 0x91176b,emissiveIntensity: 0.85,roughness: 0.55,Metalness: 0.51,side: THREE.FrontSide,});
  
  // save points for later calculation
  for (var i = 0; i < bubbleGeometry.vertices.length; i += 1) {
    var vertex = bubbleGeometry.vertices[i];
    var vec = new THREE.Vector3(vertex.x,vertex.y,vertex.z);
    sphereVerticesArray.push(vec);
    var mag = vec.x * vec.x + vec.y * vec.y + vec.z * vec.z;
    mag = Math.sqrt(mag);
    var norm = new THREE.Vector3(vertex.x / mag,vertex.y / mag,vertex.z / mag);
    sphereVerticesnormArray.push(norm);
  }
  
  bubble = new THREE.Mesh(bubbleGeometry,bubbleMaterial);
  bubble.castShadow = true;
  bubble.receiveShadow = false;
  bubble.rotation.y = -90;
  
  scene.add(bubble);
}
createBubble();


/*--------------------
Plane
--------------------*/
const createPlane = () => {
  const planeGeometry = new THREE.PlaneBufferGeometry( 2000,2000 );
  const planeMaterial = new THREE.ShadowMaterial({
    opacity: 0.15
  });
  const plane = new THREE.Mesh( planeGeometry,planeMaterial );
  plane.position.y = -150;
  plane.position.x = 0;
  plane.position.z = 0;
  plane.rotation.x = Math.PI / 180 * -90;
  plane.receiveShadow = true;
  scene.add(plane);
}
createPlane();


/*--------------------
Map
--------------------*/
const map = (num,in_min,in_max,out_min,out_max) => {
  return (num - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}


/*--------------------
distance
--------------------*/
const distance = (a,b) => {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  const d = Math.sqrt( dx * dx + dy * dy );
  return d;
}


/*--------------------
Mouse
--------------------*/
let mouse = new THREE.Vector2(0,0);
const onMouseMove = (e) => {
  TweenMax.to(mouse,0.8,{
    x : ( e.clientX / window.innerWidth ) * 2 - 1,y: - ( e.clientY / window.innerHeight ) * 2 + 1,ease: Power2.eaSEOut
  });
  
  raycaster.setFromCamera( mouse,camera );
  let intersects = raycaster.intersectObjects( scene.children );

  try{
    if ( intersects.length > 0 ) {
      if ( INTERSECTED != intersects[ 0 ].object ) {

        if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );

        INTERSECTED = intersects[ 0 ].object;
        INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
        INTERSECTED.material.emissive.setHex( 0x000000 );
        document.body.style.cursor = 'pointer';
      }
    } else {
      if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
      INTERSECTED = null;
      document.body.style.cursor = 'auto';
    }
  }catch(e){
    
  }
};
['mousemove','touchmove'].forEach(event => {
  window.addEventListener(event,onMouseMove);  
});


/*--------------------
Spring
--------------------*/
let spring = {
  scale: 1
};
const clicking = {
  down: () => {
    mouseDown = true;
  },up: () => {
    mouseDown = false;
  }
};
['mousedown','touchstart'].forEach(event => {
  window.addEventListener(event,clicking.down);
});
['mouseup','touchend'].forEach(event => {
  window.addEventListener(event,clicking.up);
});


/*--------------------
Resize
--------------------*/
const onResize = () => {
  canvas.style.width = '';
  canvas.style.height = '';
  width = canvas.offsetWidth;
  height = canvas.offsetHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix(); 
  maxdist = distance(mouse,{x: width / 2,y: height / 2});
  renderer.setSize(width,height);
}
let resizeTm;
window.addEventListener('resize',function(){
  resizeTm = clearTimeout(resizeTm);
  resizeTm = setTimeout(onResize,200);
});


/*--------------------
Noise
--------------------*/
let dist = new THREE.Vector2(0,0);
let maxdist = distance(mouse,y: height / 2});
const updateVertices = (time) => {
  dist = distance(mouse,y: height / 2});
  dist /= maxdist;
  dist = map(dist,1,1);
  for(let i = 0; i < bubbleGeometry.vertices.length; i++) {
    let vector = bubbleGeometry.vertices[i];
    vector.copy(vector.original);
    let perlin = noise.simplex3(
      (vector.x * 0.006) + (time * 0.0005),(vector.y * 0.006) + (time * 0.0005),(vector.z * 0.006)
    );
    
    let ratio = ((perlin * 0.3 * (howMuch + 0.1)) + 0.9);
    vector.multiplyScalar(ratio);
  }
  bubbleGeometry.verticesNeedUpdate = true;
}

/*--------------------
Animate
--------------------*/
const render = (a) => {
  step +=1;
  requestAnimationFrame(render);
  
  //bubble.scale.set(spring.scale,spring.scale,spring.scale);
  updateVertices(a);
  renderer.clear();
  renderer.render(scene,camera);
  
  //Activate on mouse down
  if(mouseDown && howMuch < howMuchLimit)
    howMuch += 0.01;
  else if (howMuch > 0)
    howMuch -= 0.01;
  
  if(INTERSECTED){
    if(rippleAmount < 10)
      rippleAmount += 0.05;
  }else if(rippleAmount > 0)
      rippleAmount -= 0.05;
  
  doRipple();

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

/*--------------------
Helpers
--------------------*/

function fbm(p) {
  var result = noise.simplex3(p._x,p._y,p._z);
  return result;
}

function addPoint(arr) {
  var r = new Point(0,0);
  var len = arr.length;
  for (var i = 0; i < len; i += 1) {
    r._x += arr[i]._x;
    r._y += arr[i]._y;
    r._z += arr[i]._z;
  }

  return r;
}

function Point(_x=0,_y=0,_z=0) {
  this._x = _x;
  this._y = _y;
  this._z = _z;
}

function ripple(p) {
  var q = new Point(fbm(addPoint([p,new Point(0,0)])),fbm(addPoint([p,1)])));

  return fbm(addPoint([p,new Point(0.5 * q._x,0.5 * q._y,0.5 * q._z)]));
}

function doRipple(){
  //ripple
  for (var i = 0; i < bubbleGeometry.vertices.length; i += 1) {
    var vertex = bubbleGeometry.vertices[i];

    // var value = pn.noise((vertex.x + step)/ 10,vertex.y / 10,vertex.z / 10);

    var value = ripple(new Point((vertex.x + step) / 100.0),vertex.y / 100.0,vertex.z / 100.0);

    vertex.x = sphereVerticesArray[i].x + sphereVerticesnormArray[i].x * value * rippleAmount;
    vertex.y = sphereVerticesArray[i].y + sphereVerticesnormArray[i].y * value * rippleAmount;
    vertex.z = sphereVerticesArray[i].z + sphereVerticesnormArray[i].z * value * rippleAmount;
  }
  bubbleGeometry.computeFacenormals();
  bubbleGeometry.computeVertexnormals();

  bubbleGeometry.verticesNeedUpdate = true;
  bubbleGeometry.normalsNeedUpdate = true;
}

有什么帮助吗,特别是关于鼠标指针“雕刻几何体” ,并且波更自然且来自指针

非常感谢您

解决方法

我已经调查并发现您正在与场景中的所有子项(6)相交,包括气泡阴影和灯光。 阴影似乎也与鼠标相交,触发了错误的接触

关于“ 雕刻几何”,我注意到您在初始构建期间从气泡的一个特定点硬编码了波纹效果,这就是为什么雕刻效果总是从同一点开始的原因。这是我的建议:

  • 删除硬编码的sphereVerticesArraysphereVerticesNormArray
  • 用鼠标计算出交点后,找出被击中的气泡的面:intersections[0].point以世界坐标提供交点。用它来找出联系人的脸。
  • 在波纹效应期间,使用接触面的法线作为波纹的起点和方向。

这是修复阴影相交问题的代码,其中包括一些注释:

/*--------------------
Setup
--------------------*/
console.clear();
const canvas = document.querySelector('#bubble');

//wobble
let mouseDown = false;
let howMuch = 0;
let howMuchLimit = 0.25;

//ripple
let rippleAmount = 0;
let rippleRatio = 5;
let step = 0;
let sphereVerticesArray = [];
let sphereVerticesNormArray = [];

//raycaster
let raycaster;
let isIntersectingWithBubble = false;

let width = canvas.offsetWidth,height = canvas.offsetHeight;
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,antialias: true,alpha: true
});
const scene = new THREE.Scene();

const setup = () => {
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize(width,height);
  renderer.setClearColor(0xebebeb,0);
  renderer.shadowMap.enabled = true;
  renderer.shadowMapSoft = true;

  scene.fog = new THREE.Fog(0x000000,10,950);

  const aspectRatio = width / height;
  const fieldOfView = 100;
  const nearPlane = 0.1;
  const farPlane = 10000;
  camera = new THREE.PerspectiveCamera(
    fieldOfView,aspectRatio,nearPlane,farPlane
  );
  
  raycaster = new THREE.Raycaster();
   
  camera.position.x = 0;
  camera.position.y = 0;
  camera.position.z = 300;
}
setup();


/*--------------------
Lights
--------------------*/
let hemispshereLight,shadowLight,light2;
const createLights = () => {
    hemisphereLight = new THREE.HemisphereLight(0xffffff,0x000000,.5)
  
    shadowLight = new THREE.DirectionalLight(0x666666,.4);
    shadowLight.position.set(0,450,350);
    shadowLight.castShadow = true;

    shadowLight.shadow.camera.left = -650;
    shadowLight.shadow.camera.right = 650;
    shadowLight.shadow.camera.top = 650;
    shadowLight.shadow.camera.bottom = -650;
    shadowLight.shadow.camera.near = 1;
    shadowLight.shadow.camera.far = 1000;

    shadowLight.shadow.mapSize.width = 4096;
    shadowLight.shadow.mapSize.height = 4096;
  
  light2 = new THREE.DirectionalLight(0x666666,.25);
    light2.position.set(-600,350,350);
  
  light3 = new THREE.DirectionalLight(0x666666,.15);
    light3.position.set(0,-250,300);

    scene.add(hemisphereLight);  
    scene.add(shadowLight);
  scene.add(light2);
  scene.add(light3);
}
createLights();


/*--------------------
Bubble
--------------------*/
const vertex = width > 575 ? 80 : 40;
const bubbleGeometry = new THREE.SphereGeometry( 150,vertex,vertex );
const bubbleEmissive = 0x91176b;
const bubbleEmissiveOnContact = 0x000000;

const createBubble = () => {
  for(let i = 0; i < bubbleGeometry.vertices.length; i++) {
    let vector = bubbleGeometry.vertices[i];
    vector.original = vector.clone();  
  }
  
  const bubbleMaterial = new THREE.MeshStandardMaterial({
    emissive: bubbleEmissive,emissiveIntensity: 0.85,roughness: 0.55,metalness: 0.51,side: THREE.FrontSide,});
  
  // save points for later calculation
  for (var i = 0; i < bubbleGeometry.vertices.length; i += 1) {
    var vertex = bubbleGeometry.vertices[i];
    var vec = new THREE.Vector3(vertex.x,vertex.y,vertex.z);
    sphereVerticesArray.push(vec);
    var mag = vec.x * vec.x + vec.y * vec.y + vec.z * vec.z;
    mag = Math.sqrt(mag);
    var norm = new THREE.Vector3(vertex.x / mag,vertex.y / mag,vertex.z / mag);
    sphereVerticesNormArray.push(norm);
  }
  
  const _bubble = new THREE.Mesh(bubbleGeometry,bubbleMaterial);
  _bubble.castShadow = true;
  _bubble.receiveShadow = false;
  _bubble.rotation.y = -90;
  
  scene.add(_bubble);

  return _bubble;
}
const bubble = createBubble();


/*--------------------
Plane
--------------------*/
const createPlane = () => {
  const planeGeometry = new THREE.PlaneBufferGeometry( 2000,2000 );
  const planeMaterial = new THREE.ShadowMaterial({
    opacity: 0.15
  });
  const plane = new THREE.Mesh( planeGeometry,planeMaterial );
  plane.position.y = -150;
  plane.position.x = 0;
  plane.position.z = 0;
  plane.rotation.x = Math.PI / 180 * -90;
  plane.receiveShadow = true;
  scene.add(plane);
}
createPlane();


/*--------------------
Map
--------------------*/
const map = (num,in_min,in_max,out_min,out_max) => {
  return (num - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}


/*--------------------
Distance
--------------------*/
const distance = (a,b) => {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  const d = Math.sqrt( dx * dx + dy * dy );
  return d;
}


/*--------------------
Mouse
--------------------*/
let mouse = new THREE.Vector2(0,0);
const onMouseMove = (e) => {
  TweenMax.to(mouse,0.8,{
    x : ( e.clientX / window.innerWidth ) * 2 - 1,y: - ( e.clientY / window.innerHeight ) * 2 + 1,ease: Power2.easeOut
  });
  
  raycaster.setFromCamera( mouse,camera );
  isIntersectingWithBubble = raycaster.intersectObject( bubble ).length > 0; // we are only interested in intersections with the bubble object

  try {
      if (isIntersectingWithBubble) {
          // is intersecting: change color,change pointer,change point of contact
          bubble.material.emissive.setHex(bubbleEmissiveOnContact);
          document.body.style.cursor = 'pointer';
      } else {
          // is not intersecting: restore color,restore pointer,remove point of contact
          bubble.material.emissive.setHex(bubbleEmissive);
          document.body.style.cursor = 'auto';
      }
  } catch (e) {

  }
};
['mousemove','touchmove'].forEach(event => {
  window.addEventListener(event,onMouseMove);  
});


/*--------------------
Spring
--------------------*/
let spring = {
  scale: 1
};
const clicking = {
  down: () => {
    mouseDown = true;
  },up: () => {
    mouseDown = false;
  }
};
['mousedown','touchstart'].forEach(event => {
  window.addEventListener(event,clicking.down);
});
['mouseup','touchend'].forEach(event => {
  window.addEventListener(event,clicking.up);
});


/*--------------------
Resize
--------------------*/
const onResize = () => {
  canvas.style.width = '';
  canvas.style.height = '';
  width = canvas.offsetWidth;
  height = canvas.offsetHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix(); 
  maxDist = distance(mouse,{x: width / 2,y: height / 2});
  renderer.setSize(width,height);
}
let resizeTm;
window.addEventListener('resize',function(){
  resizeTm = clearTimeout(resizeTm);
  resizeTm = setTimeout(onResize,200);
});


/*--------------------
Noise
--------------------*/
let dist = new THREE.Vector2(0,0);
let maxDist = distance(mouse,y: height / 2});
const updateVertices = (time) => {
  dist = distance(mouse,y: height / 2});
  dist /= maxDist;
  dist = map(dist,1,1);
  for(let i = 0; i < bubbleGeometry.vertices.length; i++) {
    let vector = bubbleGeometry.vertices[i];
    vector.copy(vector.original);
    let perlin = noise.simplex3(
      (vector.x * 0.006) + (time * 0.0005),(vector.y * 0.006) + (time * 0.0005),(vector.z * 0.006)
    );
    
    let ratio = ((perlin * 0.3 * (howMuch + 0.1)) + 0.9);
    vector.multiplyScalar(ratio);
  }
  bubbleGeometry.verticesNeedUpdate = true;
}

/*--------------------
Animate
--------------------*/
const render = (a) => {
  step +=1;
  requestAnimationFrame(render);
  
  //bubble.scale.set(spring.scale,spring.scale,spring.scale);
  updateVertices(a);
  renderer.clear();
  renderer.render(scene,camera);
  
  //Activate on mouse down
  if(mouseDown && howMuch < howMuchLimit)
    howMuch += 0.01;
  else if (howMuch > 0)
    howMuch -= 0.01;
  
  if(isIntersectingWithBubble){
    if(rippleAmount < 10)
      rippleAmount += 0.05;
  }else if(rippleAmount > 0)
      rippleAmount -= 0.05;
  
  doRipple();

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

/*--------------------
Helpers
--------------------*/

function fbm(p) {
  var result = noise.simplex3(p._x,p._y,p._z);
  return result;
}

function addPoint(arr) {
  var r = new Point(0,0);
  var len = arr.length;
  for (var i = 0; i < len; i += 1) {
    r._x += arr[i]._x;
    r._y += arr[i]._y;
    r._z += arr[i]._z;
  }

  return r;
}

function Point(_x=0,_y=0,_z=0) {
  this._x = _x;
  this._y = _y;
  this._z = _z;
}

function ripple(p) {
  var q = new Point(fbm(addPoint([p,new Point(0,0)])),fbm(addPoint([p,1)])));

  return fbm(addPoint([p,new Point(0.5 * q._x,0.5 * q._y,0.5 * q._z)]));
}

function doRipple(){
  //ripple
  for (var i = 0; i < bubbleGeometry.vertices.length; i += 1) {
    var vertex = bubbleGeometry.vertices[i];

    // var value = pn.noise((vertex.x + step)/ 10,vertex.y / 10,vertex.z / 10);

    var value = ripple(new Point((vertex.x + step) / 100.0),vertex.y / 100.0,vertex.z / 100.0);

    vertex.x = sphereVerticesArray[i].x + sphereVerticesNormArray[i].x * value * rippleAmount;
    vertex.y = sphereVerticesArray[i].y + sphereVerticesNormArray[i].y * value * rippleAmount;
    vertex.z = sphereVerticesArray[i].z + sphereVerticesNormArray[i].z * value * rippleAmount;
  }

  bubbleGeometry.computeFaceNormals();
  bubbleGeometry.computeVertexNormals();

  bubbleGeometry.verticesNeedUpdate = true;
  bubbleGeometry.normalsNeedUpdate = true;
}