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;">
🖱️ 拖曳旋轉 | 滾輪縮放 | 右鍵平移
</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>
使用說明#
- 複製上方程式碼區塊的完整內容
- 在
layouts/_shortcodes/資料夾中建立gaussian-splat.html檔案 - 將複製的內容貼上到檔案中
- 儲存檔案並重啟 Hugo 伺服器
