Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions config/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"1": {
"gsplatUrl": "splatting/1完成.ply",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.6,
"scale": 0.8,
"additionalInfo": {
"description": "这款 3D 打印土楼作品,以福建客家土楼为原型,精准还原了其 “外圆内方” 的经典布局与环形夯土墙结构。细节处复刻了错落的檐角、拱形窗洞与中心祖堂轮廓,用打印层叠纹理模拟夯土质感,既保留传统建筑的厚重韵味,又以轻量化材质呈现土楼的对称美学,成为可触摸的非遗建筑缩影。",
"author": "宣洗楼",
Expand All @@ -14,8 +14,8 @@
}
},
"2": {
"gsplatUrl": "splatting/2完成.ply",
"skyboxUrl": "cube/FN_HDRI_029.hdr",
"gsplatUrl": "http://qny.xitaiworkshop.space/1finish.ply",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 1,
"additionalInfo": {
"description": "城市街道场景模型,展示城市环境",
Expand All @@ -29,7 +29,7 @@
},
"3": {
"gsplatUrl": "splatting/3完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.6,
"additionalInfo": {
"description": "人物角色模型,用于演示动画和交互",
Expand All @@ -43,7 +43,7 @@
},
"4": {
"gsplatUrl": "splatting/4完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 1,
"additionalInfo": {
"description": "交通工具模型,展示精细机械结构",
Expand All @@ -57,7 +57,7 @@
},
"5": {
"gsplatUrl": "splatting/9完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.3,
"additionalInfo": {
"description": "小型道具模型,用于场景细节装饰",
Expand All @@ -71,7 +71,7 @@
},
"6": {
"gsplatUrl": "splatting/10完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.3,
"additionalInfo": {
"description": "室内场景组件,展示家居环境",
Expand All @@ -85,7 +85,7 @@
},
"7": {
"gsplatUrl": "splatting/12完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.6,
"additionalInfo": {
"description": "自然环境模型,展示自然风光",
Expand All @@ -99,7 +99,7 @@
},
"8": {
"gsplatUrl": "splatting/13完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.4,
"additionalInfo": {
"description": "特殊效果模型,展示视觉特效",
Expand All @@ -113,7 +113,7 @@
},
"9": {
"gsplatUrl": "splatting/14完成.ply",
"skyboxUrl": "cube/wide-street.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.5,
"additionalInfo": {
"description": "抽象艺术模型,展示创意设计",
Expand All @@ -127,7 +127,7 @@
},
"10": {
"gsplatUrl": "splatting/15完成.ply",
"skyboxUrl": "cube/empty-room.hdr",
"skyboxUrl": "cube/helipad-env-atlas.png",
"scale": 0.5,
"additionalInfo": {
"description": "产品展示模型,用于商业展示",
Expand Down
147 changes: 137 additions & 10 deletions controls/camera-animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,88 @@ export function animateCamera(time: number, camera: pc.Entity, targetEntity: pc.
}

/**
* 处理相机动画效果
* 相机过渡状态接口
*/
export interface CameraTransitionState {
isTransitioning: boolean;
transitionStartTime: number;
transitionDuration: number;
startPosition: pc.Vec3;
startRotation: pc.Vec3;
targetPosition: pc.Vec3;
targetRotation: pc.Vec3;
autoAnimationTime: number;
}

/**
* 计算自动动画的目标位置和旋转
*/
function calculateAutoAnimationTarget(time: number, targetEntity: pc.Entity, orbitRadius: number, horizontalRange: number, verticalRange: number): { position: pc.Vec3, rotation: pc.Vec3 } {
const horizontalAngle = horizontalRange * Math.sin(time * 0.6);
const verticalAngle = verticalRange * Math.cos(time * 0.6);

const x = orbitRadius * Math.sin(horizontalAngle);
const y = 2 + orbitRadius * Math.sin(verticalAngle);
const z = orbitRadius * Math.cos(horizontalAngle);

const position = new pc.Vec3(x, y, z);
const targetPos = targetEntity.getPosition();

// 使用与原始animateCamera相同的lookAt逻辑
// 创建一个临时相机来计算正确的旋转
const tempCamera = new pc.Entity();
tempCamera.setPosition(position);
tempCamera.lookAt(targetPos);
const rotation = tempCamera.getEulerAngles();

return { position, rotation };
}


/**
* 更新相机过渡
*/
function updateCameraTransition(
transitionState: CameraTransitionState,
camera: pc.Entity,
dt: number
): boolean {
if (!transitionState.isTransitioning) {
return false;
}

const currentTime = performance.now() / 1000;
const elapsed = currentTime - transitionState.transitionStartTime;
const progress = Math.min(elapsed / transitionState.transitionDuration, 1.0);

// 使用缓动函数(easeInOutCubic)
const easedProgress = progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;

// 插值位置
const currentPos = transitionState.startPosition.clone();
currentPos.lerp(transitionState.startPosition, transitionState.targetPosition, easedProgress);

// 插值旋转
const currentRot = transitionState.startRotation.clone();
currentRot.lerp(transitionState.startRotation, transitionState.targetRotation, easedProgress);

// 应用插值结果
camera.setPosition(currentPos);
camera.setEulerAngles(currentRot);

// 检查过渡是否完成
if (progress >= 1.0) {
transitionState.isTransitioning = false;
return true; // 过渡完成
}

return false; // 过渡进行中
}

/**
* 处理相机动画效果(带平滑过渡)
* @param dt 时间增量
* @param currentTime 当前时间
* @param autoRotate 是否自动旋转
Expand All @@ -35,7 +116,8 @@ export function animateCamera(time: number, camera: pc.Entity, targetEntity: pc.
* @param camera 相机实体
* @param targetEntity 目标实体
* @param scriptInstance 脚本实例
* @returns 更新后的时间
* @param transitionState 过渡状态
* @returns 更新后的时间、过渡状态和自动旋转状态
*/
export function handleCameraTransition(
dt: number,
Expand All @@ -45,25 +127,70 @@ export function handleCameraTransition(
autoRotateDelay: number,
camera: pc.Entity,
targetEntity: pc.Entity,
scriptInstance: any
): number {
scriptInstance: any,
transitionState?: CameraTransitionState
): { time: number, transitionState: CameraTransitionState, autoRotate: boolean } {
// 初始化过渡状态
if (!transitionState) {
transitionState = {
isTransitioning: false,
transitionStartTime: 0,
transitionDuration: 2.0,
startPosition: new pc.Vec3(),
startRotation: new pc.Vec3(),
targetPosition: new pc.Vec3(),
targetRotation: new pc.Vec3(),
autoAnimationTime: 0
};
}

let time = currentTime;
time += dt;

// 检查是否应该恢复自动转动
if (!autoRotate && time - lastMouseActivityTime > autoRotateDelay) {
const shouldAutoRotate = !autoRotate && time - lastMouseActivityTime > autoRotateDelay;

// 开始过渡到自动动画
if (shouldAutoRotate) {
const currentTarget = calculateAutoAnimationTarget(time, targetEntity, 6, Math.PI / 6, Math.PI / 12);

// 设置过渡状态
transitionState.isTransitioning = true;
transitionState.transitionStartTime = performance.now() / 1000;
transitionState.transitionDuration = 2.0;
transitionState.startPosition.copy(camera.getPosition());
transitionState.startRotation.copy(camera.getEulerAngles());
transitionState.targetPosition.copy(currentTarget.position);
transitionState.targetRotation.copy(currentTarget.rotation);

transitionState.autoAnimationTime = time;
autoRotate = true;
}


// 根据状态决定是自动转动还是由脚本控制
// 更新过渡
if (transitionState.isTransitioning) {
const transitionComplete = updateCameraTransition(transitionState, camera, dt);
if (transitionComplete) {
transitionState.isTransitioning = false;
}
return { time, transitionState, autoRotate };
}

// 执行相机动画
if (autoRotate) {
// 自动转动摄像头
animateCamera(time, camera, targetEntity, 6, Math.PI / 6, Math.PI / 12);
if (transitionState.autoAnimationTime > 0) {
animateCamera(transitionState.autoAnimationTime, camera, targetEntity, 6, Math.PI / 6, Math.PI / 12);
transitionState.autoAnimationTime += dt;
} else {
animateCamera(time, camera, targetEntity, 6, Math.PI / 6, Math.PI / 12);
}
} else {
if (scriptInstance) {
scriptInstance.skipUpdate = false;

}
transitionState.autoAnimationTime = 0;
}

return time; // 返回更新后的时间
return { time, transitionState, autoRotate };
}
Loading