首頁 > 軟體

Three.js實現雪糕地球的使用範例詳解

2022-07-05 18:03:22

前言

最近的天氣有幾分酷熱,去實驗室的道路也有幾分漫長,走著走著,小包感覺靈魂已經被熱出竅了。回到實驗室,把空調開啟,雪糕吃上,靜坐了幾分鐘,才重新感覺到靈魂的滋味,葛優躺在實驗室的小床上,思維開始天馬行空,世上有一萬種方式能讓小包涼快,但地球母親吶,她卻日漸炎熱,誰能來給她降降溫?

躺著躺著,進入了夢鄉,小包夢到未來有一天,人類超級發達,可以穿梭時空,但發展的代價也是巨大的,地球母親不堪重負,熱度超標,我們卻束手無策,科學家最後想出一個古老的辦法,將地球的一週用冰包裹起來,進行物理降溫。這很讓人驚悚,小包醒來後,枯坐了一會,決定做一個雪糕地球,不只是一種整活調侃,也是一種反思與警示,保護地球,人人有責。

  • style
* {
    -webkit-user-select: none;
       -moz-user-select: none;
        -ms-user-select: none;
            user-select: none;
  }
  body {
    height: 100vh;
    background-color: hotpink;
    margin: 0;
    padding: 0;
    overflow: hidden;
  }
  .loader {
    display: flex;
    color: white;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 5em;
    width: 100%;
    height: 100%;
    font-family: "Baloo Bhaijaan", cursive;
  }
  .loader span {
    text-shadow: 0 1px #bbb, 0 2px #bbb, 0 3px #bbb, 0 4px #bbb, 0 5px #bbb,
      0 6px transparent, 0 7px transparent, 0 8px transparent,
      0 9px transparent, 0 10px 10px rgba(0, 0, 0, 0.4);
    text-shadow: 0 1px #bbb, 0 2px #bbb, 0 3px #bbb, 0 4px #bbb,
        0 5px #bbb, 0 6px #bbb, 0 7px #bbb, 0 8px #bbb, 0 9px #bbb,
        0 50px 25px rgba(0, 0, 0, 0.2);
      transform: translateY(-20px);
  }
  •  script
/*
 * 基礎設定
 */
let isLoaded = false; // 紋理資源是否載入完畢
const loadingScreen = {
  scene: new THREE.Scene(),
  camera: new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  ),
  // 移除載入標誌的函數
  removeText() {
    const loadingText = document.querySelector("#canvas-loader");
    if (loadingText.parentNode) {
      loadingText.parentNode.removeChild(loadingText);
    }
  },
};
// 初始化載入器
let loadingManager = new THREE.LoadingManager();
// 監聽載入器 onLoad 事件
loadingManager.onLoad = () => {
  loadingScreen.removeText();
  isLoaded = true;
};
// 建立場景
const scene = new THREE.Scene();
// 建立渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
// 渲染器基本設定
renderer.setClearColor("hotpink");
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// canvas 外部容器
const canvasWrapper = document.querySelector("#canvas-wrapper");
// 建立透視相機
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
// 設定相機位置
camera.position.set(0, 0, 220);
// 建立平行光源
const light = new THREE.DirectionalLight();
light.position.set(0, 0, 1);
scene.add(light);
// 建立點光源
const point = new THREE.PointLight(0xeeeeee);
point.position.set(400, 200, 300); //點光源位置
scene.add(point); //點光源新增到場景中
// 建立球體
const cRadius = 100;
const geometry = new THREE.SphereBufferGeometry(
  cRadius,
  cRadius * 6.4,
  cRadius * 6.4
);
// 紋理圖
const textureLoader = new THREE.TextureLoader(loadingManager);
const textureSurface = textureLoader.load(
  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/249663/world-surface.jpg"
);
const textureElevation = textureLoader.load(
  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/249663/world-elevation.jpg"
);
const textureSpecular = textureLoader.load(
  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/249663/world-specular.jpg"
);
// 材質資訊
const materialOpt = {
  map: textureSurface,
  normalMap: textureElevation,
  specularMap: textureSpecular,
  shininess: 80,
};
const material = new THREE.MeshPhongMaterial(materialOpt);
// 建立網格體
const sphere = new THREE.Mesh(geometry, material);
// 設定環境貼圖的顏色深淺
sphere.material.normalScale.set(0.5, 0.5);
// 將模型新增到場景中
scene.add(sphere);
// 將 canvas 元素新增到頁面中
canvasWrapper.appendChild(renderer.domElement);
/*
 * 事件監聽實現動效
 */
let mouseX = 0;
let mouseY = 0;
const moveAnimate = {
  coordinates(clientX, clientY) {
    const limit = 270;
    const limitNeg = limit * -1;
    mouseX = clientX - window.innerWidth / 2;
    mouseY = clientY - window.innerHeight / 2;
    mouseX = mouseX >= limit ? limit : mouseX;
    mouseX = mouseX <= limitNeg ? limitNeg : mouseX;
    mouseY = mouseY >= limit ? limit : mouseY;
    mouseY = mouseY <= limitNeg ? limitNeg : mouseY;
  },
  onMouseMove(e) {
    moveAnimate.coordinates(e.clientX, e.clientY);
  },
  onTouchMove(e) {
    const touchX = e.changedTouches[0].clientX;
    const touchY = e.changedTouches[0].clientY;
    moveAnimate.coordinates(touchX, touchY);
  },
};
document.addEventListener("mousemove", moveAnimate.onMouseMove);
document.addEventListener("touchmove", moveAnimate.onTouchMove);
const onWindowResize = () => {
  const w = window.innerWidth;
  const h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
};
window.addEventListener("resize", onWindowResize);
const createAnimRotation = () => {
  const speed = 0.005;
  sphere.rotation.z += speed / 2;
  sphere.rotation.y += speed;
};
// 渲染函數
const render = () => {
  if (!isLoaded) {
    renderer.render(loadingScreen.scene, loadingScreen.camera);
    requestAnimationFrame(render);
    return;
  }
  camera.position.x += (mouseX * -1 - camera.position.x) * 0.05;
  camera.position.y += (mouseY - camera.position.y) * 0.05;
  camera.lookAt(scene.position);
  createAnimRotation();
  renderer.render(scene, camera);
  requestAnimationFrame(render);
};
render();

ThreeJS 基礎——實現轉動的球體

Three.js 是一款執行在瀏覽器中的 3D 引擎,你可以用它建立各種三維場景,包括了攝影機、光影、材質等各種物件,大家或多或少應該都見識過 Three 的傳說。這是小包第一次使用 Three,因此小包會圍繞雪糕地球實現的各種細節講起。

下面首先來看一下 Three 框架的基本組成要素(圖源: Three.js 教學)

Three 中最重要的三個物件即場景、相機和渲染器。場景即放置模型、光照的場地;相機設定以何種方式何種角度來觀看場景,渲染器將效果渲染到網頁中。這三個概念都不難理解,下面我們用程式碼實現這三個物件。

// 場景
const scene = new THREE.Scene();
// 透視相機
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
// 渲染器
const renderer = new THREE.WebGLRenderer();
// 設定渲染區域尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
// body元素中插入canvas物件
document.body.appendChild(renderer.domElement);
// 設定背景顏色
renderer.setClearColor("hotpink");
// 執行渲染操作   指定場景、相機作為引數
renderer.render(scene, camera);

Three 中有多種相機,本文章主要使用透視相機(PerspectiveCamera),其原理與人眼所看的景象類似,共有四個引數:

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

fov: 表示能看到的角度範圍,值為角度,類似於人的視角。

aspect: 表示渲染視窗的長寬比,如果網頁中只有一個 canvas,其值通常設定為網頁視口的寬高比

near/far: near/far 分別代表攝像機的近剪下面和遠剪下面

文字有些難以理解,可以參考一下下圖:

開啟瀏覽器,看一下會渲染出什麼?目前只能看到全粉色的網頁,這是因為目前的場景中並沒有新增 3D 模型物件。

接下來我們來新增一個球體模型,作為地球的基底。

const cRadius = 100;
const geometry = new THREE.SphereBufferGeometry(
  cRadius,
  cRadius * 6.4,
  cRadius * 6.4
);

SphereBufferGeometryThree 中實現球體的 API,引數非常多,這裡只介紹前三個引數

radius: 球體半徑

widthSegments: 沿經線方向分段數

heightSegments: 沿緯線方向分段數

為球體新增材質 Material,目前我們只新增一個顏色屬性。

// 材質物件Material
const material = new THREE.MeshLambertMaterial({
  color: 0x0000ff,
});

渲染網格體 Mesh,並將其新增到場景 Scene 中。

// 網格體 Mesh,兩個引數分別為幾何體和材質
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);

重新開啟網站,並沒有看到球體,還是一片粉茫茫的寂寥,天理何在?

Three 相機的初始位置預設為 (0,0,0),相機焦點預設為 Z 軸負半軸方向,球體的半徑是 100,也就是說目前相機位於球體內部,因此我們需要調整相機位置。

// 設定相機的位置
camera.position.set(0, 0, 220);
// 設定相機焦點的方向
camera.lookAt(scene.position);

噹噹噹當,網頁中就可以成功看到一個黑色球體了,額有點奇怪,我們明明設定的是 0x0000ff 顏色,怎麼會顯示一個黑色模型?

小包苦思冥想: 萬物本沒有顏色,顏色是光的反射。在整個場景中,目前是沒有光源的,因此下面分別新增平行光(DirectionalLight)和點光源(PointLight)

平行光是沿著特定方向發射的光,其表現類似無限遠的陽光,文章使用它來模擬太陽光。點光源是從一個點向各個方向發射的光源,使用它來增加整體的亮度。

// 宣告平行光
const light = new THREE.DirectionalLight();
// 設定平行光源位置
light.position.set(0, 0, 1);
// 將平行光源新增到場景中
scene.add(light);
// 宣告點光源
const point = new THREE.PointLight(0xeeeeee);
// 設定點光源位置
point.position.set(400, 200, 300);
// 點光源新增到場景中
scene.add(point);

立體效果看起來不明顯,沒事,接下來我們讓球體動起來。接下來,給球體新增一個 z 軸和 y 軸的轉動。

const createAnimRotation = () =&gt; {
  const speed = 0.005;
  sphere.rotation.z += speed / 2;
  sphere.rotation.y += speed;
};
const render = () =&gt; {
  createAnimRotation();
  renderer.render(scene, camera);
  requestAnimationFrame(render);
};
render();

由於球體是對稱的,轉動看起來並不明顯,如果你特別想看到轉動效果,可以將 SphereBufferGeometry 暫時更換為 BoxBufferGeometry

ThreeJS 紋理——實現轉動的地球

上文已經成功實現地球,接下來我們來為地球披上衣服。本文實現的是雪糕地球,因此小包直接為其披上雪糕外衣。

Three 可以將一張紋理圖對映到幾何體上,具體的對映原理我們不做探究,對映的思想可以參考下圖。

選取一張雪糕地球的紋理圖,使用下面的程式碼實現紋理貼圖效果。

// 紋理載入器物件
const textureLoader = new THREE.TextureLoader();
const textureSurface = textureLoader.load(
  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/249663/world-surface.jpg"
);
// 設定紋理貼圖
const material = new THREE.MeshLambertMaterial({ map: textureSurface });

只使用普通貼圖的雪糕地球看起來已經非常不錯了,但還有進一步美化的空間,Three 提供了高光貼圖,使用高光貼圖,會有高亮部分顯示。

const textureSpecular = textureLoader.load(
  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/249663/world-specular.jpg"
);
const material = new THREE.MeshPhongMaterial({
  map: textureSurface,
  specularMap: textureSpecular,
  shininess: 80, // 高光部分的亮度
});

雖然動圖錄制的幀數太低,還是依稀可以看到一些高亮區域。

Three 還提供了環境貼圖,環境貼圖可以增加表面的細節,使三維模型更加立體。

const textureElevation = textureLoader.load(
  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/249663/world-elevation.jpg"
);
const material = new THREE.MeshPhongMaterial({
  map: textureSurface,
  normalMap: textureElevation,
  specularMap: textureSpecular,
  shininess: 80,
});

立體效果是有了,但是具體看起來一言難盡,顏色有些許暗淡,不符合雪糕的風格。

小包繼續開始檢視檔案,環境貼圖中有 normalScale 屬性,可以設定顏色的深淺程度,減少對應屬性值為 0.5,0.5

sphere.material.normalScale.set(0.5, 0.5);

互動式雪糕地球

給地球加一些互動效果:

  • 當滑鼠靠近,地球放大;滑鼠遠離時,地球縮小
  • 地球隨滑鼠方向轉動

上述動效我們可以通過移動相機位置實現。首先設定地球旋轉的最大正負角度為 270

// 定義動效物件
let mouseX = 0;
let mouseY = 0;
const moveAnimate = {
  coordinates(clientX, clientY) {
    const limit = 270;
    const limitNeg = limit * -1;
    mouseX = clientX - window.innerWidth / 2;
    mouseY = clientY - window.innerHeight / 2;
    mouseX = mouseX &gt;= limit ? limit : mouseX;
    mouseX = mouseX &lt;= limitNeg ? limitNeg : mouseX;
    mouseY = mouseY &gt;= limit ? limit : mouseY;
    mouseY = mouseY &lt;= limitNeg ? limitNeg : mouseY;
  },
  onMouseMove(e) {
    moveAnimate.coordinates(e.clientX, e.clientY);
  },
};
document.addEventListener("mousemove", moveAnimate.onMouseMove);

通過上述事件計算出 mouseXmouseY 的值,在 render 函數中,修改 camera 的位置。

camera.position.x += (mouseX * -1 - camera.position.x) * 0.05;
camera.position.y += (mouseY - camera.position.y) * 0.05;
camera.lookAt(scene.position);

行動端同步監聽 touchmove 事件,手機也可以看到雪糕地球的動態效果。

const moveAnimate = {
  onTouchMove(e) {
    const touchX = e.changedTouches[0].clientX;
    const touchY = e.changedTouches[0].clientY;
    moveAnimate.coordinates(touchX, touchY);
  },
};
document.addEventListener("touchmove", moveAnimate.onTouchMove);

新增 loading 效果

紋理的載入需要一定的時間,因此新增一個轉場 loading 效果。

loading 效果使用小包前面的實現躍動的文字中的效果。

.loader {
  display: flex;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 5em;
  width: 100%;
  height: 100%;
  font-family: "Baloo Bhaijaan", cursive;
}
.loader span {
  text-shadow: 0 1px #bbb, 0 2px #bbb, 0 3px #bbb, 0 4px #bbb, 0 5px #bbb, 0 6px
      transparent, 0 7px transparent, 0 8px transparent, 0 9px transparent, 0
      10px 10px rgba(0, 0, 0, 0.4);
  text-shadow: 0 1px #bbb, 0 2px #bbb, 0 3px #bbb, 0 4px #bbb, 0 5px #bbb, 0 6px
      #bbb, 0 7px #bbb, 0 8px #bbb, 0 9px #bbb, 0 50px 25px rgba(0, 0, 0, 0.2);
  transform: translateY(-20px);
}

Three 提供了 LoadingManager,其功能是處理並跟蹤已載入和待處理的資料。當所有載入器載入完成後,會呼叫 LoadingManager 上的 onLoad 事件。

因此我們定義一個 LoadingManager,當觸發 onLoad 事件後,將頁面中的載入標誌移除。

const loadingScreen = {
  scene: new THREE.Scene(),
  camera: new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  ),
  // 移除載入標誌的函數
  removeText() {
    const loadingText = document.querySelector("#canvas-loader");
    if (loadingText.parentNode) {
      loadingText.parentNode.removeChild(loadingText);
    }
  },
};
// 初始化載入器
let loadingManager = new THREE.LoadingManager();
// 監聽載入器 onLoad 事件
loadingManager.onLoad = () =&gt; {
  loadingScreen.removeText();
  isLoaded = true;
};
// 紋理圖載入器傳入 loadingManager
const textureLoader = new THREE.TextureLoader(loadingManager);

參考連結

Three中文檔案

以上就是Three.js實現雪糕地球的使用範例詳解的詳細內容,更多關於Three.js雪糕地球的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com