第10章-二次开发入门
第十章:二次开发入门
10.1 二次开发概述
Astral3D提供了灵活的二次开发能力,开发者可以基于SDK构建自己的3D应用,或者对编辑器进行深度定制。本章将介绍二次开发的基础知识和入门流程。
10.1.1 开发模式
| 开发模式 | 说明 | 适用场景 |
|---|---|---|
| SDK集成 | 将SDK集成到自己的项目中 | 构建独立的3D应用 |
| 编辑器扩展 | 通过插件扩展编辑器功能 | 增加新工具和面板 |
| 脚本开发 | 使用脚本实现运行时逻辑 | 自定义交互和动画 |
| 源码修改 | 直接修改编辑器源代码 | 深度定制需求 |
10.1.2 SDK架构
┌─────────────────────────────────────────────────────────────────┐
│ @astral3d/engine SDK │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Core (核心模块) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Viewer │ │ Scene │ │ Camera │ │ Control │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Loader (加载模块) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ GLTF │ │ FBX │ │ IFC │ │ CAD │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Effect (特效模块) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Particle │ │Weather │ │ Post │ │Highlight│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Utils (工具模块) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Measure │ │Raycast │ │ Export │ │ Math │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10.2 SDK集成
10.2.1 创建项目
使用Vite创建Vue3项目:
# 创建项目
npm create vite@latest my-3d-app -- --template vue-ts
# 进入项目
cd my-3d-app
# 安装依赖
npm install
# 安装Astral3D SDK
npm install @astral3d/engine three
npm install -D @types/three
10.2.2 项目结构
my-3d-app/
├── src/
│ ├── components/
│ │ ├── Viewer3D.vue # 3D视图组件
│ │ ├── Toolbar.vue # 工具栏组件
│ │ └── PropertyPanel.vue # 属性面板组件
│ ├── composables/
│ │ └── useViewer.ts # Viewer组合式函数
│ ├── utils/
│ │ └── helpers.ts # 工具函数
│ ├── App.vue
│ ├── main.ts
│ └── style.css
├── public/
│ └── models/ # 模型文件
├── package.json
├── tsconfig.json
└── vite.config.ts
10.2.3 基础集成示例
Viewer组合式函数:
// src/composables/useViewer.ts
import { ref, onMounted, onUnmounted, Ref } from 'vue';
import { Viewer, ViewerOptions } from '@astral3d/engine';
import * as THREE from 'three';
export function useViewer(container: Ref<HTMLElement | null>) {
const viewer = ref<Viewer | null>(null);
const isReady = ref(false);
// 初始化Viewer
const init = async () => {
if (!container.value) return;
viewer.value = new Viewer({
container: container.value,
antialias: true,
alpha: false,
shadowMap: true
});
await viewer.value.init();
// 添加默认灯光
addDefaultLights();
// 添加网格辅助
addGridHelper();
isReady.value = true;
};
// 添加默认灯光
const addDefaultLights = () => {
if (!viewer.value) return;
// 环境光
const ambient = new THREE.AmbientLight(0x404040, 0.5);
viewer.value.scene.add(ambient);
// 主灯光
const directional = new THREE.DirectionalLight(0xffffff, 1);
directional.position.set(50, 50, 50);
directional.castShadow = true;
viewer.value.scene.add(directional);
};
// 添加网格辅助
const addGridHelper = () => {
if (!viewer.value) return;
const grid = new THREE.GridHelper(100, 100, 0x444444, 0x222222);
viewer.value.scene.add(grid);
};
// 加载模型
const loadModel = async (url: string) => {
if (!viewer.value) return null;
const model = await viewer.value.loader.load(url);
viewer.value.scene.add(model);
// 自动聚焦
viewer.value.cameraController.focusOn(model);
return model;
};
// 清理
const dispose = () => {
if (viewer.value) {
viewer.value.dispose();
viewer.value = null;
}
};
onMounted(init);
onUnmounted(dispose);
return {
viewer,
isReady,
loadModel
};
}
3D视图组件:
<!-- src/components/Viewer3D.vue -->
<template>
<div class="viewer-container" ref="containerRef">
<div v-if="!isReady" class="loading">
加载中...
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useViewer } from '../composables/useViewer';
const containerRef = ref<HTMLElement | null>(null);
const { viewer, isReady, loadModel } = useViewer(containerRef);
// 暴露给父组件
defineExpose({
viewer,
loadModel
});
</script>
<style scoped>
.viewer-container {
width: 100%;
height: 100%;
position: relative;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #666;
}
</style>
应用入口:
<!-- src/App.vue -->
<template>
<div class="app">
<header class="header">
<h1>我的3D应用</h1>
<div class="toolbar">
<button @click="handleLoadModel">加载模型</button>
<button @click="handleResetView">重置视图</button>
</div>
</header>
<main class="main">
<Viewer3D ref="viewerRef" />
</main>
<aside class="sidebar">
<h3>属性面板</h3>
<!-- 属性面板内容 -->
</aside>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Viewer3D from './components/Viewer3D.vue';
const viewerRef = ref();
const handleLoadModel = async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.gltf,.glb,.fbx,.obj';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const url = URL.createObjectURL(file);
await viewerRef.value.loadModel(url);
}
};
input.click();
};
const handleResetView = () => {
viewerRef.value.viewer?.cameraController.reset();
};
</script>
<style>
.app {
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: 1fr 300px;
height: 100vh;
}
.header {
grid-column: 1 / -1;
padding: 10px 20px;
background: #333;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
}
.main {
background: #1a1a1a;
}
.sidebar {
background: #2a2a2a;
padding: 20px;
color: white;
}
.toolbar button {
margin-left: 10px;
padding: 8px 16px;
cursor: pointer;
}
</style>
10.3 Viewer配置
10.3.1 完整配置选项
interface ViewerOptions {
// 容器配置
container: HTMLElement | string; // 渲染容器
width?: number; // 宽度(默认auto)
height?: number; // 高度(默认auto)
// 渲染器配置
antialias?: boolean; // 抗锯齿(默认true)
alpha?: boolean; // 透明背景(默认false)
preserveDrawingBuffer?: boolean; // 保留绘制缓冲(截图用)
powerPreference?: 'high-performance' | 'low-power' | 'default';
// 阴影配置
shadowMap?: boolean; // 启用阴影(默认true)
shadowMapType?: THREE.ShadowMapType; // 阴影类型
shadowMapSize?: number; // 阴影贴图大小
// 色调映射
toneMapping?: THREE.ToneMapping; // 色调映射类型
toneMappingExposure?: number; // 曝光度
outputColorSpace?: THREE.ColorSpace; // 输出颜色空间
// 相机配置
cameraType?: 'perspective' | 'orthographic';
fov?: number; // 视场角(默认60)
near?: number; // 近裁面(默认0.1)
far?: number; // 远裁面(默认10000)
cameraPosition?: THREE.Vector3; // 初始位置
cameraTarget?: THREE.Vector3; // 初始目标
// 控制器配置
enableControls?: boolean; // 启用控制器
controlsType?: 'orbit' | 'fly' | 'firstPerson';
enableDamping?: boolean; // 启用阻尼
dampingFactor?: number; // 阻尼系数
enableZoom?: boolean; // 启用缩放
enableRotate?: boolean; // 启用旋转
enablePan?: boolean; // 启用平移
// 功能开关
enableStats?: boolean; // 性能统计
enableSelection?: boolean; // 选择功能
enableOutline?: boolean; // 轮廓高亮
enableAxesHelper?: boolean; // 坐标轴辅助
enableGridHelper?: boolean; // 网格辅助
}
10.3.2 配置示例
// 高质量渲染配置
const highQualityOptions: ViewerOptions = {
container: '#viewer',
antialias: true,
shadowMap: true,
shadowMapType: THREE.PCFSoftShadowMap,
shadowMapSize: 2048,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1.5,
outputColorSpace: THREE.SRGBColorSpace
};
// 性能优先配置
const performanceOptions: ViewerOptions = {
container: '#viewer',
antialias: false,
shadowMap: false,
powerPreference: 'high-performance'
};
// 移动端配置
const mobileOptions: ViewerOptions = {
container: '#viewer',
antialias: false,
shadowMap: false,
powerPreference: 'low-power',
enableDamping: true,
dampingFactor: 0.1
};
10.4 常用操作
10.4.1 模型操作
import { Viewer } from '@astral3d/engine';
import * as THREE from 'three';
const viewer = new Viewer({ container: '#viewer' });
// 加载模型
const model = await viewer.loader.load('model.glb');
viewer.scene.add(model);
// 移动模型
model.position.set(10, 0, 0);
// 旋转模型
model.rotation.set(0, Math.PI / 4, 0);
// 缩放模型
model.scale.set(2, 2, 2);
// 查找子对象
const part = model.getObjectByName('Part1');
// 修改材质
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshStandardMaterial({
color: 0xff0000,
metalness: 0.5,
roughness: 0.5
});
}
});
// 克隆模型
const clone = model.clone();
clone.position.x += 5;
viewer.scene.add(clone);
// 移除模型
viewer.scene.remove(model);
// 销毁模型(释放资源)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry.dispose();
child.material.dispose();
}
});
10.4.2 相机操作
// 设置相机位置
viewer.camera.position.set(10, 5, 10);
viewer.camera.lookAt(0, 0, 0);
// 获取相机位置
const position = viewer.camera.position.clone();
const target = viewer.cameraController.target.clone();
// 相机飞行动画
await viewer.cameraController.flyTo(
new THREE.Vector3(20, 10, 20), // 目标位置
new THREE.Vector3(0, 0, 0), // 观察目标
2000 // 动画时长(ms)
);
// 聚焦到对象
viewer.cameraController.focusOn(model, 1.5);
// 切换视图
viewer.cameraController.setView('top');
viewer.cameraController.setView('front');
viewer.cameraController.setView('right');
viewer.cameraController.setView('perspective');
// 重置视图
viewer.cameraController.reset();
// 保存/恢复视图
const viewState = viewer.cameraController.saveState();
viewer.cameraController.restoreState(viewState);
10.4.3 选择与高亮
// 启用选择
viewer.selection.enabled = true;
// 监听选择变化
viewer.selection.onSelect.add((objects) => {
console.log('选中:', objects.map(o => o.name));
});
// 程序化选择
viewer.selection.select(model);
viewer.selection.deselect();
// 设置高亮样式
viewer.highlighter.setStyle({
color: 0xff0000,
opacity: 0.3,
edgeStrength: 3
});
// 高亮对象
viewer.highlighter.highlight(model);
// 取消高亮
viewer.highlighter.unhighlight(model);
viewer.highlighter.unhighlightAll();
10.4.4 事件处理
// 鼠标点击
viewer.on('click', (event) => {
const intersects = viewer.raycast(event.clientX, event.clientY);
if (intersects.length > 0) {
const object = intersects[0].object;
console.log('点击:', object.name);
}
});
// 双击
viewer.on('dblclick', (event) => {
const intersects = viewer.raycast(event.clientX, event.clientY);
if (intersects.length > 0) {
viewer.cameraController.focusOn(intersects[0].object);
}
});
// 悬停
viewer.on('hover', (event) => {
const intersects = viewer.raycast(event.clientX, event.clientY);
if (intersects.length > 0) {
viewer.highlighter.highlight(intersects[0].object);
} else {
viewer.highlighter.unhighlightAll();
}
});
// 键盘事件
document.addEventListener('keydown', (event) => {
if (event.key === 'Delete') {
const selected = viewer.selection.getSelected();
selected.forEach(obj => viewer.scene.remove(obj));
}
});
10.5 自定义组件
10.5.1 工具栏组件
<!-- src/components/Toolbar.vue -->
<template>
<div class="toolbar">
<div class="tool-group">
<button
v-for="tool in tools"
:key="tool.id"
:class="{ active: activeTool === tool.id }"
@click="selectTool(tool.id)"
:title="tool.name"
>
<span class="icon">{{ tool.icon }}</span>
</button>
</div>
<div class="separator"></div>
<div class="tool-group">
<button @click="$emit('undo')" title="撤销">↩</button>
<button @click="$emit('redo')" title="重做">↪</button>
</div>
<div class="separator"></div>
<div class="tool-group">
<button @click="$emit('zoomFit')" title="适应窗口">⊡</button>
<button @click="$emit('toggleGrid')" title="网格">⊞</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const tools = [
{ id: 'select', name: '选择', icon: '⬚' },
{ id: 'move', name: '移动', icon: '✥' },
{ id: 'rotate', name: '旋转', icon: '⟳' },
{ id: 'scale', name: '缩放', icon: '⤢' }
];
const activeTool = ref('select');
const emit = defineEmits(['toolChange', 'undo', 'redo', 'zoomFit', 'toggleGrid']);
const selectTool = (id: string) => {
activeTool.value = id;
emit('toolChange', id);
};
</script>
<style scoped>
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #333;
}
.tool-group {
display: flex;
gap: 4px;
}
.separator {
width: 1px;
height: 24px;
background: #555;
}
button {
width: 36px;
height: 36px;
border: none;
border-radius: 4px;
background: transparent;
color: white;
cursor: pointer;
font-size: 18px;
}
button:hover {
background: #444;
}
button.active {
background: #0066cc;
}
</style>
10.5.2 属性面板组件
<!-- src/components/PropertyPanel.vue -->
<template>
<div class="property-panel">
<div v-if="!selectedObject" class="empty">
未选择对象
</div>
<template v-else>
<div class="section">
<h4>基本信息</h4>
<div class="property">
<label>名称</label>
<input v-model="objectName" @change="updateName" />
</div>
<div class="property">
<label>类型</label>
<span>{{ selectedObject.type }}</span>
</div>
</div>
<div class="section">
<h4>变换</h4>
<div class="property">
<label>位置</label>
<div class="vector3">
<input type="number" v-model.number="position.x" @change="updateTransform" />
<input type="number" v-model.number="position.y" @change="updateTransform" />
<input type="number" v-model.number="position.z" @change="updateTransform" />
</div>
</div>
<div class="property">
<label>旋转</label>
<div class="vector3">
<input type="number" v-model.number="rotation.x" @change="updateTransform" />
<input type="number" v-model.number="rotation.y" @change="updateTransform" />
<input type="number" v-model.number="rotation.z" @change="updateTransform" />
</div>
</div>
<div class="property">
<label>缩放</label>
<div class="vector3">
<input type="number" v-model.number="scale.x" @change="updateTransform" />
<input type="number" v-model.number="scale.y" @change="updateTransform" />
<input type="number" v-model.number="scale.z" @change="updateTransform" />
</div>
</div>
</div>
<div class="section">
<h4>显示</h4>
<div class="property">
<label>可见</label>
<input type="checkbox" v-model="visible" @change="updateVisibility" />
</div>
<div class="property">
<label>投射阴影</label>
<input type="checkbox" v-model="castShadow" @change="updateShadow" />
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import * as THREE from 'three';
const props = defineProps<{
selectedObject: THREE.Object3D | null;
}>();
const objectName = ref('');
const position = reactive({ x: 0, y: 0, z: 0 });
const rotation = reactive({ x: 0, y: 0, z: 0 });
const scale = reactive({ x: 1, y: 1, z: 1 });
const visible = ref(true);
const castShadow = ref(false);
// 监听选中对象变化
watch(() => props.selectedObject, (obj) => {
if (obj) {
objectName.value = obj.name;
position.x = obj.position.x;
position.y = obj.position.y;
position.z = obj.position.z;
rotation.x = THREE.MathUtils.radToDeg(obj.rotation.x);
rotation.y = THREE.MathUtils.radToDeg(obj.rotation.y);
rotation.z = THREE.MathUtils.radToDeg(obj.rotation.z);
scale.x = obj.scale.x;
scale.y = obj.scale.y;
scale.z = obj.scale.z;
visible.value = obj.visible;
castShadow.value = obj.castShadow;
}
}, { immediate: true });
const updateName = () => {
if (props.selectedObject) {
props.selectedObject.name = objectName.value;
}
};
const updateTransform = () => {
if (props.selectedObject) {
props.selectedObject.position.set(position.x, position.y, position.z);
props.selectedObject.rotation.set(
THREE.MathUtils.degToRad(rotation.x),
THREE.MathUtils.degToRad(rotation.y),
THREE.MathUtils.degToRad(rotation.z)
);
props.selectedObject.scale.set(scale.x, scale.y, scale.z);
}
};
const updateVisibility = () => {
if (props.selectedObject) {
props.selectedObject.visible = visible.value;
}
};
const updateShadow = () => {
if (props.selectedObject) {
props.selectedObject.castShadow = castShadow.value;
}
};
</script>
<style scoped>
.property-panel {
padding: 16px;
color: white;
}
.empty {
text-align: center;
color: #666;
padding: 20px;
}
.section {
margin-bottom: 20px;
}
.section h4 {
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #444;
}
.property {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.property label {
width: 60px;
font-size: 12px;
color: #aaa;
}
.property input[type="text"],
.property input[type="number"] {
flex: 1;
padding: 4px 8px;
background: #333;
border: 1px solid #444;
border-radius: 4px;
color: white;
}
.vector3 {
display: flex;
gap: 4px;
flex: 1;
}
.vector3 input {
width: 60px;
padding: 4px;
text-align: center;
}
</style>
10.6 数据管理
10.6.1 场景序列化
// 序列化场景
function serializeScene(scene: THREE.Scene) {
const data = {
objects: [],
lights: [],
cameras: []
};
scene.traverse((obj) => {
if (obj instanceof THREE.Mesh) {
data.objects.push({
name: obj.name,
type: 'mesh',
position: obj.position.toArray(),
rotation: obj.rotation.toArray(),
scale: obj.scale.toArray(),
geometry: obj.geometry.toJSON(),
material: serializeMaterial(obj.material)
});
} else if (obj instanceof THREE.Light) {
data.lights.push({
name: obj.name,
type: obj.type,
position: obj.position.toArray(),
color: obj.color.getHex(),
intensity: obj.intensity
});
}
});
return data;
}
// 反序列化场景
async function deserializeScene(data: any, scene: THREE.Scene) {
// 清空现有场景
while (scene.children.length > 0) {
scene.remove(scene.children[0]);
}
// 恢复对象
for (const objData of data.objects) {
const geometry = new THREE.BufferGeometry().fromJSON(objData.geometry);
const material = deserializeMaterial(objData.material);
const mesh = new THREE.Mesh(geometry, material);
mesh.name = objData.name;
mesh.position.fromArray(objData.position);
mesh.rotation.fromArray(objData.rotation);
mesh.scale.fromArray(objData.scale);
scene.add(mesh);
}
// 恢复灯光
for (const lightData of data.lights) {
const light = createLight(lightData);
scene.add(light);
}
}
10.6.2 状态管理
// 使用Pinia管理状态
import { defineStore } from 'pinia';
export const useSceneStore = defineStore('scene', {
state: () => ({
selectedObjects: [] as THREE.Object3D[],
clipboard: null as THREE.Object3D | null,
history: [] as any[],
historyIndex: -1
}),
actions: {
select(objects: THREE.Object3D[]) {
this.selectedObjects = objects;
},
deselect() {
this.selectedObjects = [];
},
copy() {
if (this.selectedObjects.length > 0) {
this.clipboard = this.selectedObjects[0].clone();
}
},
paste(scene: THREE.Scene) {
if (this.clipboard) {
const clone = this.clipboard.clone();
clone.position.x += 1;
scene.add(clone);
return clone;
}
return null;
},
pushHistory(action: any) {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(action);
this.historyIndex++;
},
undo() {
if (this.historyIndex >= 0) {
const action = this.history[this.historyIndex];
action.undo();
this.historyIndex--;
}
},
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
const action = this.history[this.historyIndex];
action.redo();
}
}
}
});
10.7 本章小结
本章介绍了Astral3D的二次开发入门知识,主要内容包括:
- 开发模式:SDK集成、编辑器扩展、脚本开发、源码修改
- SDK集成:项目创建、结构组织、基础集成示例
- Viewer配置:完整配置选项和不同场景的配置示例
- 常用操作:模型操作、相机操作、选择高亮、事件处理
- 自定义组件:工具栏组件、属性面板组件
- 数据管理:场景序列化、状态管理
通过本章的学习,读者应该能够开始基于Astral3D SDK进行二次开发,构建自己的3D应用。
下一章预告:第十一章将介绍Astral3D的二次开发进阶内容,包括自定义加载器、渲染扩展、性能优化等高级主题。

浙公网安备 33010602011771号