快轉到主要內容

Gaussian Splat Shortcode 完整程式碼

·6 分鐘·
目錄

Gaussian Splat Shortcode 完整程式碼
#

以下是 layouts/_shortcodes/gaussian-splat.html 的完整程式碼內容。

重要提醒: 此頁面僅供參考,請直接從專案中的 layouts/_shortcodes/gaussian-splat.html 檔案複製程式碼,或從此頁面複製下方程式碼區塊的內容。

完整程式碼
#

{{/* 
  Gaussian Splatting Viewer Shortcode (GaussianSplats3D version)
  
  Parameters:
  - id: unique identifier (optional, auto-generated if not provided)
  - src: path to .ply or .splat file (required)
  - height: container height (default: 500px)
  - static: set to true if file is in /static folder (default: false, uses page bundle)
  - cameraX, cameraY, cameraZ: 相機位置 (default: 0, 0, 5)
  - lookAtX, lookAtY, lookAtZ: 相機看向的位置 (default: 0, 0, 0)
  - cameraUpX, cameraUpY, cameraUpZ: 相機上方方向 (default: 0, -1, 0)
  - modelX, modelY, modelZ: 模型位置偏移 (default: 0, 0, 0)
  - modelScale: 模型縮放比例 (default: 1)
  - rotateX, rotateY, rotateZ: 模型旋轉角度(度數,用於修正座標軸方向)(default: 0, 0, 0)
  - coordinateSystem: 座標系統預設 ("y-up" 或 "z-up", default: "y-up")
  - autoFit: 自動適配模型並調整相機 (default: false, 設為 true 可解決座標問題)
*/}}

{{ $id := .Get "id" | default (printf "gs-%d" now.UnixNano) }}
{{ $srcParam := .Get "src" }}
{{ $height := .Get "height" | default "500px" }}
{{ $isStatic := .Get "static" | default false }}

{{/* 相機參數 - 支援自訂角度 */}}
{{ $cameraX := .Get "cameraX" | default "0" }}
{{ $cameraY := .Get "cameraY" | default "0" }}
{{ $cameraZ := .Get "cameraZ" | default "5" }}
{{ $lookAtX := .Get "lookAtX" | default "0" }}
{{ $lookAtY := .Get "lookAtY" | default "0" }}
{{ $lookAtZ := .Get "lookAtZ" | default "0" }}
{{ $cameraUpX := .Get "cameraUpX" | default "0" }}
{{ $cameraUpY := .Get "cameraUpY" | default "-1" }}
{{ $cameraUpZ := .Get "cameraUpZ" | default "0" }}

{{/* 模型變換參數 - 解決座標問題 */}}
{{ $modelX := .Get "modelX" | default "0" }}
{{ $modelY := .Get "modelY" | default "0" }}
{{ $modelZ := .Get "modelZ" | default "0" }}
{{ $modelScale := .Get "modelScale" | default "1" }}
{{ $rotateX := .Get "rotateX" | default "0" }}
{{ $rotateY := .Get "rotateY" | default "0" }}
{{ $rotateZ := .Get "rotateZ" | default "0" }}
{{ $coordinateSystem := .Get "coordinateSystem" | default "y-up" }}
{{ $autoFit := .Get "autoFit" | default "false" }}

{{/* 處理檔案路徑 - Page Bundle 或 Static */}}
{{ $src := "" }}
{{ if $isStatic }}
  {{ $src = $srcParam }}
{{ else }}
  {{ $resource := .Page.Resources.GetMatch $srcParam }}
  {{ if $resource }}
    {{ $src = $resource.RelPermalink }}
  {{ else }}
    {{ $src = $srcParam }}
  {{ end }}
{{ end }}

<div id="gs-container-{{ $id }}" class="gaussian-splat-container" style="width: 100%; height: {{ $height }}; background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); border-radius: 12px; overflow: hidden; position: relative; margin: 1.5rem 0;">
  <div id="gs-canvas-{{ $id }}" style="width: 100%; height: 100%;"></div>
  <div id="gs-loading-{{ $id }}" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #58a6ff; font-family: system-ui, -apple-system, sans-serif; text-align: center; z-index: 10;">
    <div style="width: 40px; height: 40px; border: 3px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: gs-spin 1s linear infinite; margin: 0 auto 12px;"></div>
    <div>載入 3D Gaussian Splatting 模型中...</div>
    <div id="gs-progress-{{ $id }}" style="font-size: 14px; color: #8b949e; margin-top: 8px;">0%</div>
  </div>
  <div id="gs-error-{{ $id }}" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #f85149; font-family: system-ui, -apple-system, sans-serif; text-align: center; display: none; max-width: 80%; padding: 20px; z-index: 10;">
  </div>
  <div id="gs-controls-{{ $id }}" style="position: absolute; bottom: 12px; left: 12px; color: #8b949e; font-size: 12px; font-family: system-ui, -apple-system, sans-serif; background: rgba(13, 17, 23, 0.8); padding: 8px 12px; border-radius: 6px; display: none; z-index: 10;">
    🖱️ 拖曳旋轉 &nbsp;|&nbsp; 滾輪縮放 &nbsp;|&nbsp; 右鍵平移
  </div>
</div>

<style>
  @keyframes gs-spin {
    to { transform: rotate(360deg); }
  }
</style>

<!-- Import Map for ES Modules -->
<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.164.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.164.0/examples/jsm/"
  }
}
</script>

<script type="module">
  // GaussianSplats3D and THREE
  import * as GaussianSplats3D from 'https://unpkg.com/@mkkellogg/gaussian-splats-3d@0.4.6/build/gaussian-splats-3d.module.js';
  import * as THREE from 'three';

  const containerId = 'gs-container-{{ $id }}';
  const canvasId = 'gs-canvas-{{ $id }}';
  const loadingId = 'gs-loading-{{ $id }}';
  const progressId = 'gs-progress-{{ $id }}';
  const controlsId = 'gs-controls-{{ $id }}';
  const errorId = 'gs-error-{{ $id }}';
  
  const container = document.getElementById(containerId);
  const canvasContainer = document.getElementById(canvasId);
  const loadingEl = document.getElementById(loadingId);
  const progressEl = document.getElementById(progressId);
  const controlsEl = document.getElementById(controlsId);
  const errorEl = document.getElementById(errorId);

  const modelSrc = '{{ $src }}';
  
  console.log('GaussianSplats3D: Loading model from:', modelSrc);

  async function init() {
    try {
      // 解析相機參數
      const cameraPosition = [
        parseFloat('{{ $cameraX }}'),
        parseFloat('{{ $cameraY }}'),
        parseFloat('{{ $cameraZ }}')
      ];
      const cameraLookAt = [
        parseFloat('{{ $lookAtX }}'),
        parseFloat('{{ $lookAtY }}'),
        parseFloat('{{ $lookAtZ }}')
      ];
      const cameraUp = [
        parseFloat('{{ $cameraUpX }}'),
        parseFloat('{{ $cameraUpY }}'),
        parseFloat('{{ $cameraUpZ }}')
      ];

      console.log('Camera settings:', {
        position: cameraPosition,
        lookAt: cameraLookAt,
        up: cameraUp
      });

      const viewer = new GaussianSplats3D.Viewer({
        cameraUp: cameraUp,
        initialCameraPosition: cameraPosition,
        initialCameraLookAt: cameraLookAt,
        rootElement: canvasContainer,
        sharedMemoryForWorkers: false,
      });

      // 模型變換參數
      const modelPosition = [
        parseFloat('{{ $modelX }}'),
        parseFloat('{{ $modelY }}'),
        parseFloat('{{ $modelZ }}')
      ];
      const modelScale = parseFloat('{{ $modelScale }}');
      const rotateX = parseFloat('{{ $rotateX }}');
      const rotateY = parseFloat('{{ $rotateY }}');
      const rotateZ = parseFloat('{{ $rotateZ }}');
      const coordinateSystem = '{{ $coordinateSystem }}';
      const useAutoFit = '{{ $autoFit }}' === 'true';

      const sceneConfig = {
        splatAlphaRemovalThreshold: 5,
        showLoadingUI: false,
        progressiveLoad: true,
        onProgress: (progress, message, stage) => {
          // progress 已經是 0-100 的百分比值
          const percent = Math.min(100, Math.round(progress));
          progressEl.textContent = `${percent}%`;
        }
      };

      // 如果設定了模型位置或縮放,加入變換參數
      if (modelPosition[0] !== 0 || modelPosition[1] !== 0 || modelPosition[2] !== 0) {
        sceneConfig.position = modelPosition;
      }
      if (modelScale !== 1) {
        sceneConfig.scale = [modelScale, modelScale, modelScale];
      }

      // 計算旋轉四元數(用於修正座標軸方向)
      let rotationQuaternion = new THREE.Quaternion();
      
      // 如果設定了座標系統預設,應用對應的旋轉
      if (coordinateSystem === 'z-up') {
        // Z-up 轉 Y-up:繞 X 軸旋轉 -90 度
        rotationQuaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -Math.PI / 2);
      }
      
      // 應用手動旋轉(以度為單位轉換為弧度)
      if (rotateX !== 0 || rotateY !== 0 || rotateZ !== 0) {
        const quatX = new THREE.Quaternion().setFromAxisAngle(
          new THREE.Vector3(1, 0, 0), 
          (rotateX * Math.PI) / 180
        );
        const quatY = new THREE.Quaternion().setFromAxisAngle(
          new THREE.Vector3(0, 1, 0), 
          (rotateY * Math.PI) / 180
        );
        const quatZ = new THREE.Quaternion().setFromAxisAngle(
          new THREE.Vector3(0, 0, 1), 
          (rotateZ * Math.PI) / 180
        );
        // 組合旋轉:先應用座標系統轉換,再應用手動旋轉
        // 旋轉順序:先 X,再 Y,最後 Z(歐拉角順序)
        const manualRotation = new THREE.Quaternion();
        manualRotation.multiply(quatX).multiply(quatY).multiply(quatZ);
        rotationQuaternion.multiply(manualRotation);
      }
      
      // 如果有旋轉,加入旋轉參數
      if (!rotationQuaternion.equals(new THREE.Quaternion())) {
        sceneConfig.rotation = rotationQuaternion.toArray();
        console.log('Model rotation applied:', rotationQuaternion.toArray());
      }

      await viewer.addSplatScene(modelSrc, sceneConfig);

      // 自動適配功能:計算模型邊界並調整相機
      if (useAutoFit) {
        try {
          // 等待一幀確保場景已完全載入
          await new Promise(resolve => requestAnimationFrame(resolve));
          
          // 嘗試從 viewer 獲取場景物件
          // GaussianSplats3D 的場景通常在 viewer.splatMesh 或 viewer.scene 中
          let splatMesh = null;
          if (viewer.splatMesh) {
            splatMesh = viewer.splatMesh;
          } else if (viewer.scene && viewer.scene.children && viewer.scene.children.length > 0) {
            // 尋找第一個包含 splat 的物件
            for (let child of viewer.scene.children) {
              if (child.type === 'Group' || child.children) {
                splatMesh = child;
                break;
              }
            }
          }
          
          if (splatMesh) {
            // 計算模型的邊界框(bounding box)
            const box = new THREE.Box3();
            box.setFromObject(splatMesh);
            
            if (!box.isEmpty()) {
              const center = box.getCenter(new THREE.Vector3());
              const size = box.getSize(new THREE.Vector3());
              const maxDim = Math.max(size.x, size.y, size.z);
              
              // 根據模型大小自動調整相機距離
              const distance = maxDim * 2.5;
              const cameraPos = new THREE.Vector3(
                center.x + distance * 0.5,
                center.y + distance * 0.3,
                center.z + distance * 0.7
              );
              
              // 更新相機位置和看向點
              if (viewer.camera) {
                viewer.camera.position.copy(cameraPos);
                viewer.camera.lookAt(center);
              }
              if (viewer.controls) {
                viewer.controls.target.copy(center);
                viewer.controls.update();
              }
              
              console.log('Auto-fit applied:', {
                center: [center.x, center.y, center.z],
                size: [size.x, size.y, size.z],
                cameraPos: [cameraPos.x, cameraPos.y, cameraPos.z]
              });
            }
          } else {
            console.warn('Auto-fit: Could not find splat mesh, using default camera');
          }
        } catch (e) {
          console.warn('Auto-fit failed, using default camera:', e);
        }
      }

      // 載入完成
      loadingEl.style.display = 'none';
      controlsEl.style.display = 'block';
      viewer.start();
      
      console.log('GaussianSplats3D: Model loaded successfully');

    } catch (error) {
      console.error('GaussianSplats3D error:', error);
      loadingEl.style.display = 'none';
      errorEl.style.display = 'block';
      errorEl.innerHTML = `
        <div style="font-size: 24px; margin-bottom: 12px;">⚠️</div>
        <div style="margin-bottom: 8px;">載入模型失敗</div>
        <div style="font-size: 12px; color: #8b949e;">${error.message}</div>
        <div style="font-size: 11px; color: #6e7681; margin-top: 12px;">檔案路徑: ${modelSrc}</div>
      `;
    }
  }

  init();
</script>

使用說明
#

  1. 複製上方程式碼區塊的完整內容
  2. layouts/_shortcodes/ 資料夾中建立 gaussian-splat.html 檔案
  3. 將複製的內容貼上到檔案中
  4. 儲存檔案並重啟 Hugo 伺服器
David Chang
作者
David Chang