第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的二次开发入门知识,主要内容包括:

  1. 开发模式:SDK集成、编辑器扩展、脚本开发、源码修改
  2. SDK集成:项目创建、结构组织、基础集成示例
  3. Viewer配置:完整配置选项和不同场景的配置示例
  4. 常用操作:模型操作、相机操作、选择高亮、事件处理
  5. 自定义组件:工具栏组件、属性面板组件
  6. 数据管理:场景序列化、状态管理

通过本章的学习,读者应该能够开始基于Astral3D SDK进行二次开发,构建自己的3D应用。


下一章预告:第十一章将介绍Astral3D的二次开发进阶内容,包括自定义加载器、渲染扩展、性能优化等高级主题。


posted @ 2026-01-10 13:17  我才是银古  阅读(21)  评论(0)    收藏  举报