Commit 9dff46d5 by xhw

Initial commit

parents
File added
File added
File added
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D高斯泼溅 - 视角探索</title>
<style>
* { margin: 0; padding: 0; }
body { overflow: hidden; background: #1a1a2e; font-family: 'Segoe UI', Arial, sans-serif; }
canvas { display: block; }
</style>
</head>
<body>
<script type="module">
import * as THREE from "./js/three.module.js";
import { SparkRenderer, SplatMesh } from "./js/spark.module.js";
import { OrbitControls } from "./js/OrbitControls.js";
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 2. 场景 / 相机 / 渲染器
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
let zoomAnimId = null;
// let autoCropEnabled = false;
const aspect = window.innerWidth / window.innerHeight;
// ── 透视相机 ──
const perspCamera = new THREE.PerspectiveCamera(20, aspect, 0.001, 10000);
// ★ 修复:初始位置设为 Z 轴方向,与模型坐标系一致
perspCamera.position.set(0, 0, 5);
let camera = perspCamera;
const renderer = new THREE.WebGLRenderer({
antialias: false,
alpha: false // ★ 修复3:关闭 alpha,避免背景混合产生边缘杂色
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
document.body.appendChild(renderer.domElement);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 3. 轨道控制器 —— 核心修复区域
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.maxDistance = 100;
controls.minDistance = 0.01;
// 限制转动范围:上下左右各10度
controls.minPolarAngle = Math.PI / 2 - Math.PI / 6;
controls.maxPolarAngle = Math.PI / 2 + Math.PI / 6;
controls.minAzimuthAngle = -Math.PI / 10;
controls.maxAzimuthAngle = Math.PI / 10;
controls.target.set(0, 0, 0);
// ★ 修复:设置正确的"上方向",确保旋转行为符合直觉
controls.object.up.set(0, 0, 0); // Y 轴向上(标准设置)
// 取消鼠标右键拖动(平移)
controls.mouseButtons = {
LEFT: THREE.MOUSE.ROTATE,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: null
};
controls.update();
// 保存初始视角
const HOME_POS = perspCamera.position.clone();
const HOME_TARGET = controls.target.clone();
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 4. Spark 渲染器
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const spark = new SparkRenderer({
renderer,
maxStdDev: Math.sqrt(5), // 限制高斯核范围,提升性能
sortRadial: true, // 径向排序,快速旋转时避免黑边(默认就是 true)
enableLod: true, // 启用 LoD(大场景必需)
lodSplatScale: 1.0 // 细节倍率
});
scene.add(spark);
const splatMesh = new SplatMesh({
url: './Advance.spz',
onLoad: (mesh) => {
console.log("模型加载完成");
fitCameraToModel();
const state = getDeviceType();
state === "desktop" ? animateZoomTo(2.2, 500) : animateZoomTo(0.55, 500);
// 加载完成后,把相机从当前位置往模型方向(-Z)挪
controls.update();
},
onProgress: (event) => {
if (event.lengthComputable) {
const pct = ((event.loaded / event.total) * 100).toFixed(1);
console.log(`加载进度: ${pct}%`);
}
}
});
scene.add(splatMesh);
splatMesh.rotation.x = -Math.PI;
function getModelBounds() {
if (!splatMesh) return null;
const box = new THREE.Box3();
splatMesh.traverse((child) => {
if (child.isMesh && child.geometry) {
box.expandByObject(child);
} else if (child.isPoints && child.geometry) {
box.expandByObject(child);
}
});
if (box.isEmpty()) {
box.setFromObject(splatMesh);
}
return box;
}
function fitPerspectiveToModel() {
const box = getModelBounds();
console.log("模型包围盒:", box);
if (!box || box.isEmpty()) {
console.warn("无法计算包围盒,使用默认范围");
const size = 0.3;
const dist = size / (2 * Math.tan((perspCamera.fov * Math.PI) / 360));
perspCamera.position.set(0, 0, dist * 3);
controls.target.set(0, 0, 0);
controls.update();
return;
}
const center = new THREE.Vector3();
box.getCenter(center);
const size = new THREE.Vector3();
box.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
controls.target.copy(center);
const fovRad = (perspCamera.fov * Math.PI) / 180;
const dist = maxDim / (2 * Math.tan(fovRad / 2)) * 0.25;
// ★ 修复:从 Z 轴方向观察(标准前视图)
// 保持相机在 Z 轴方向,Y 轴向上
const newPos = center.clone();
newPos.z += Math.max(dist, 0.1); // 沿 +Z 方向后退
// 也可以根据需要从其他角度观察:
// newPos.y += dist; // 从上方俯视
// newPos.x += dist; // 从侧面观察
perspCamera.position.copy(newPos);
// ★ 修复:确保相机看向模型中心
perspCamera.lookAt(center);
controls.update();
controls.maxDistance = dist * 3;
controls.minDistance = maxDim * 0.1;
HOME_POS.copy(perspCamera.position);
HOME_TARGET.copy(controls.target);
}
function fitCameraToModel() {
fitPerspectiveToModel();
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 7. 渲染循环
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
// 暴露到全局供调试
window.__scene = scene;
window.__camera = camera;
window.__controls = controls;
window.__splat = splatMesh;
// 平滑过渡 zoom
function animateZoomTo(targetZoom, duration = 500) {
if (zoomAnimId) cancelAnimationFrame(zoomAnimId);
const startZoom = camera.zoom;
const startTime = performance.now();
function step(time) {
const t = Math.min((time - startTime) / duration, 1);
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
camera.zoom = startZoom + (targetZoom - startZoom) * ease;
camera.updateProjectionMatrix();
if (t < 1) {
zoomAnimId = requestAnimationFrame(step);
} else {
zoomAnimId = null;
updateInfo();
}
}
zoomAnimId = requestAnimationFrame(step);
}
function updateInfo() {
const p = camera.position;
const t = controls.target;
const r = camera.rotation;
console.log(`相机位置: (${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)})`);
console.log(`旋转中心: (${t.x.toFixed(2)}, ${t.y.toFixed(2)}, ${t.z.toFixed(2)})`);
}
function getDeviceType() {
const ua = navigator.userAgent;
const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua) ||
(/Android/i.test(ua) && window.innerWidth > 768);
if (/Mobi|Android|iPhone/i.test(ua) && !isTablet) return 'mobile';
if (isTablet) return 'tablet';
return 'desktop';
}
window.addEventListener("resize", () => {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D高斯泼溅 - SparkSplatViewer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: #1a1a2e;
font-family: 'Segoe UI', Arial, sans-serif;
width: 100vw;
height: 100vh;
}
#viewer-container {
width: 100%;
height: 100%;
}
</style>
<base target="_blank">
<base target="_blank">
</head>
<body>
<div id="viewer-container"></div>
<script type="module">
import { SparkSplatViewer } from "./SparkSplatViewer.js";
// 初始化组件
const container = document.getElementById("viewer-container");
const viewer = new SparkSplatViewer(container, {
// 场景配置
backgroundColor: 0x1a1a2e,
// 相机配置
fov: 45,
fovMobile: 20,
initialPosition: { x: 0, y: 0, z: 5 },
// 控制器配置
enablePan: false,
minPolarAngle: Math.PI / 2 - Math.PI / 6,
maxPolarAngle: Math.PI / 2 + Math.PI / 6,
minAzimuthAngle: -Math.PI / 10,
maxAzimuthAngle: Math.PI / 10,
// 自动旋转配置(新增)
autoRotate: true, // 开启自动旋转
autoRotateSpeed: 0.1, // 速度(推荐 0.2~0.5)
autoRotateIdleDelay: 2000, // 空闲多久后恢复(ms)
// 模型配置
defaultModelUrl: "./Advance.spz",
modelRotationX: -Math.PI,
// 缩放配置
desktopZoom: 2.5,
mobileZoom: 0.55,
tabletZoom: 1.0,
// 回调
onLoad: (mesh) => {
console.log("默认模型加载完成", mesh);
},
onError: (err) => {
console.error("模型加载失败", err);
},
});
// 暴露到全局,方便调试和外部调用
window.viewer = viewer;
// ═══════════════════════════════════════════════════
// 加载新模型的方法(供外部调用)
// ═══════════════════════════════════════════════════
/**
* 加载新模型并替换当前模型
* @param {string} url - 模型文件路径 (.spz, .ply, .splat, .ksplat, .sog, .rad 等)
* @param {Object} options - 可选配置
*/
window.loadNewModel = (url, options = {}) => {
viewer.loadModel(url, {
autoFit: options.autoFit !== false, // 默认自动适配相机
zoomDuration: options.zoomDuration ?? 500, // 缩放动画时长
desktopZoom: options.desktopZoom ?? 2.2, // 桌面端缩放
mobileZoom: options.mobileZoom ?? 0.55, // 移动端缩放
tabletZoom: options.tabletZoom ?? 1.0, // 平板端缩放
onComplete: options.onComplete, // 加载完成回调
onError: options.onError, // 加载失败回调
onProgress: options.onProgress, // 进度回调
});
};
// 使用示例:
// loadNewModel("./new-model.spz");
// loadNewModel("./new-model.spz", { autoFit: true, desktopZoom: 3.0 });
// 其他可用方法:
// viewer.resetCamera(); // 重置相机
// viewer.fitCameraToModel(); // 适配相机到模型
// viewer.getCameraInfo(); // 获取相机信息
// viewer.animateZoomTo(2.0, 800); // 平滑缩放到指定值
// viewer.destroy(); // 销毁组件
// 获取模型包围盒
// const bounds = viewer.getModelBounds();
</script>
</body>
</html>
\ No newline at end of file
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
import {
BufferGeometry,
Float32BufferAttribute,
OrthographicCamera,
Mesh
} from '../three.module.js';
/**
* Abstract base class for all post processing passes.
*
* This module is only relevant for post processing with {@link WebGLRenderer}.
*
* @abstract
* @three_import import { Pass } from 'three/addons/postprocessing/Pass.js';
*/
class Pass {
/**
* Constructs a new pass.
*/
constructor() {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPass = true;
/**
* If set to `true`, the pass is processed by the composer.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
/**
* If set to `true`, the pass indicates to swap read and write buffer after rendering.
*
* @type {boolean}
* @default true
*/
this.needsSwap = true;
/**
* If set to `true`, the pass clears its buffer before rendering
*
* @type {boolean}
* @default false
*/
this.clear = false;
/**
* If set to `true`, the result of the pass is rendered to screen. The last pass in the composers
* pass chain gets automatically rendered to screen, no matter how this property is configured.
*
* @type {boolean}
* @default false
*/
this.renderToScreen = false;
}
/**
* Sets the size of the pass.
*
* @abstract
* @param {number} width - The width to set.
* @param {number} height - The height to set.
*/
setSize( /* width, height */ ) {}
/**
* This method holds the render logic of a pass. It must be implemented in all derived classes.
*
* @abstract
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*
* @abstract
*/
dispose() {}
}
// Helper for passes that need to fill the viewport with a single quad.
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
// https://github.com/mrdoob/three.js/pull/21358
class FullscreenTriangleGeometry extends BufferGeometry {
constructor() {
super();
this.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
}
}
const _geometry = new FullscreenTriangleGeometry();
/**
* This module is a helper for passes which need to render a full
* screen effect which is quite common in context of post processing.
*
* The intended usage is to reuse a single full screen quad for rendering
* subsequent passes by just reassigning the `material` reference.
*
* This module can only be used with {@link WebGLRenderer}.
*
* @augments Mesh
* @three_import import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
*/
class FullScreenQuad {
/**
* Constructs a new full screen quad.
*
* @param {?Material} material - The material to render te full screen quad with.
*/
constructor( material ) {
this._mesh = new Mesh( _geometry, material );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the instance is no longer used in your app.
*/
dispose() {
this._mesh.geometry.dispose();
}
/**
* Renders the full screen quad.
*
* @param {WebGLRenderer} renderer - The renderer.
*/
render( renderer ) {
renderer.render( this._mesh, _camera );
}
/**
* The quad's material.
*
* @type {?Material}
*/
get material() {
return this._mesh.material;
}
set material( value ) {
this._mesh.material = value;
}
}
export { Pass, FullScreenQuad };
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
File added
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment