iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

在 Day 10 中,我們成功封裝了強制鎖定最近鄰取樣的 loadPixelTexture,徹底解決了像素貼圖模糊的問題,並且透過 BoxGeometry 的六面材質陣列,組裝出了頂面為壓頂石、側面為立面石磚的複合材質。

然而,當我們要將資料陣列 mapGrid 真正轉化為 3D 場景實體時,迎面而來的便是 3D 像素遊戲最常見的陷阱——貼圖拉伸(Texture Stretching)。很多開發者為了讓牆壁變高,習慣直接設定 tile.scale.y = 3,但這會讓貼圖在垂直方向被生硬放大 3 倍,使得 16×16 的正方形像素被拉扯成細長的長方形條紋,摧毀整體的像素美感。今天我們將透過幾何座標的中心化換算,將「平坦地面與障礙物」與「立體古堡石牆」的生成邏輯徹底拆開,實作一套保持像素 1:1 比例的模組化垂直堆疊系統

【Step 1:幾何定義與地圖中心化座標換算】

在生成任何 3D 網格前,我們需要一個標準的單位立方體幾何體,並計算方塊在 3D 空間中的座標。 我們宣告 tileSize = 1,並建立單一共享的立方體幾何體 tileGeo = new THREE.BoxGeometry(tileSize, 1, tileSize)。由於 50x50 的網格在迴圈中的索引是 0 ~ 49,如果直接將 col 與 row 當作 X 與 Z 座標,整座地下城的原點 (0, 0, 0) 會落在地圖的左上角邊界,不利於鏡頭的對稱觀看。因此,我們透過中心化位移公式將整張地圖置中:

image

公式中的 - (mapSize / 2) 會將索引往負軸位移一半的長度,而 + 0.5 則是將方塊的錨點精準對齊到網格中心,使整個 50x50 的地城中心恰好座落在世界座標的 (0, 0, 0)。

【Step 2:地面圖塊生成與碎石障礙物(tileType === 2)立體化】

在第一輪雙層迴圈中,我們專注於鋪設「地板」與「地表障礙物」。最關鍵的架構原則是:代碼 5 的牆壁實體絕對不在這裡建立。一旦掃描到 tileType === 5,我們使用 continue 直接跳過,留給專屬的建築建構函式處理。

// ==========================================
// 4. 3D 實體生成 (逐層堆疊無拉伸 + 陰影支援)
// ==========================================
const tileSize = 1; //
const tileGeo = new THREE.BoxGeometry(tileSize, 1, tileSize); //
const mapSize = mapGrid.length; //

// 生成地面與障礙物
for (let row = 0; row < mapSize; row++) {
  for (let col = 0; col < mapGrid[row].length; col++) {
    const tileType = mapGrid[row][col]; //
    const posX = col - (mapSize / 2) + 0.5; //
    const posZ = row - (mapSize / 2) + 0.5; //

    if (tileType === 5) {
      // 牆壁實體由下方程式專門建構 (避免拉伸)
      continue; //
    }

    const currentMaterial = materials[tileType] || materials[0]; //
    const tile = new THREE.Mesh(tileGeo, currentMaterial); //
    tile.position.set(posX, 0.5, posZ); //
    tile.receiveShadow = true; //

    if (tileType === 2) {
      // 凸起的碎石障礙
      tile.scale.set(0.9, 1.3, 0.9); //
      tile.position.y = 1.3 / 2; //
      tile.castShadow = true; //
    }

    scene.add(tile); //
  }
}

【Step 3:1x1 模組化立體堆疊——像素長寬比的絕對防線】
解決了地板後,我們展開第二輪雙層迴圈,專門建構代碼為 5 的立體城牆。

為了徹底消滅材質垂直拉伸,我們採用「實體堆疊法」:若一堵牆壁高度為 3,我們就在該座標垂直疊放 3 個標準的 1x1x1 立方體,其垂直中心分別座落在 Y = 0.5、Y = 1.5 與 Y = 2.5。這樣無論牆壁堆得多高,每一格方塊的側面紋理始終保持 1:1 的完美正方形,像素密度(Texel Density)完全一致。

// 專屬建構無拉伸的古堡石牆 (逐層模組化 1x1 堆疊,保持 16px 像素比例完美對齊)
for (let row = 0; row < mapSize; row++) {
  for (let col = 0; col < mapGrid[row].length; col++) {
    if (mapGrid[row][col] !== 5) continue; 

    const height = wallHeightMap[row][col] || 2;  
    const posX = col - (mapSize / 2) + 0.5; 
    const posZ = row - (mapSize / 2) + 0.5; 

    for (let layer = 0; layer < height; layer++) {
      const isTop = (layer === height - 1); 
      let blockMat;

      if (isTop) {
        // 頂層方塊:帶壓頂石
        const isMossy = Math.random() < 0.25; 
        blockMat = isMossy ? wallCapMossMaterials : wallCapMaterials; 
      } else {
        // 牆身中下層:深藍石磚或青苔石磚
        blockMat = (layer === 0 && Math.random() < 0.3) ? wallMossMat : wallFaceMat; //[cite: 7]
      }

      const block = new THREE.Mesh(tileGeo, blockMat); 
      block.position.set(posX, layer + 0.5, posZ); 
      block.castShadow = true; 
      block.receiveShadow = true; 
      scene.add(block); 
    }
  }
}

【Step 4:分層材質判斷——頂面壓頂石與牆根青苔】
在垂直堆疊的 layer 迴圈中,每一層方塊並不是套用一成不變的材質,而是依照所處的層級動態指派:

頂層判斷 (isTop = layer === height - 1):只有位於建築最上層的方塊,才會套用 Day 10 封裝的多面材質陣列。這裡加入 Math.random() < 0.25 的機率判定,有 25% 的機率會切換成帶有裂紋壓頂石與青苔的 wallCapMossMaterials,其餘 75% 則套用工整的 wallCapMaterials 。這讓俯瞰視角下的城牆頂部充滿古老磚石的破損細節。

中下層牆身判斷:非頂層的方塊屬於牆體立面,不需要壓頂石。我們在最底層(layer === 0)加入 Math.random() < 0.3 的機率判定,讓 30% 貼近地面的牆磚替換為潮濕的青苔立面 wallMossMat,其餘與中層則統一套用深藍古石磚 wallFaceMat。

完整雙向陰影支援:每個生成的牆體方塊都同時開啟 block.castShadow = true 與 block.receiveShadow = true。這代表方塊不僅能阻擋光源向地面投下影子,也能接收來自上方相鄰方塊的遮蔽陰影,為未來的光影渲染做好準備。

【本日結語】
透過 1x1 模組化積木堆疊工法,我們徹底摒棄了粗暴的 scale.y 縮放,捍衛了 16×16 像素藝術的尊嚴。現在地城中的每一塊磚石都具備正確的正方形長寬比,頂部有平整收邊的壓頂石,牆腳帶有隨機蔓延的青苔。
明天(Day 12),我們將推進城牆的建築演算法——利用預設的遺跡段落陣列,計算出中心完整、兩端倒塌的高低漸變破壞層次,並在斷口處實作隨機掉落碎石的動態機制!


上一篇
Day 10:捍衛像素尊嚴!貼圖銳利化載入器與 Box 多材質陣列拆解
下一篇
Day 12:廢墟建築演算法!殘破古堡的高低起伏、轉角延伸與碎石掉落機制
系列文
Three.js × WebGL 實戰:打造 HD-2D 像素地下城與即時光影系統 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言