feat(SDK & Editor): 为3D Tiles添加侧边配置栏

This commit is contained in:
ErSan 2025-10-07 22:33:16 +08:00
parent 28a950478b
commit 1f7b232730
15 changed files with 260 additions and 44 deletions

View File

@ -395,10 +395,6 @@ export default {
'Depth Write': '深度缓冲', 'Depth Write': '深度缓冲',
Wireframe: '线框', Wireframe: '线框',
}, },
"Scene theme": "场景主题",
sceneTheme: {
'Blue tone': '蓝田日暖玉生烟',
},
Animations: '动画', Animations: '动画',
animation: { animation: {
Play: '播放', Play: '播放',
@ -542,6 +538,12 @@ export default {
"Html panel":"Html面板", "Html panel":"Html面板",
htmlPanel: { htmlPanel: {
Content: "内容" Content: "内容"
},
"3D Tiles": "3D Tiles",
tiles: {
"Color mode": "颜色模式",
"Box bounds": "盒边界",
"Sphere bounds": "球形边界",
} }
}, },
assets: { assets: {
@ -606,7 +608,8 @@ export default {
Copy: "复制", Copy: "复制",
Focus: '聚焦', Focus: '聚焦',
Support: '支持', Support: '支持',
Upload: '上传' Upload: '上传',
Debug:"调试",
}, },
/* 提示 */ /* 提示 */
prompt: { prompt: {

View File

@ -88,7 +88,6 @@ async function handlePreview(item){
// url:import.meta.env.VITE_GLOB_ORIGIN + item.tileset, // url:import.meta.env.VITE_GLOB_ORIGIN + item.tileset,
// name:previewInfo.name, // name:previewInfo.name,
// reset2origin:true, // reset2origin:true,
// debug:false,
// }) // })
// //
// previewer.addTiles(tiles).then(() => { // previewer.addTiles(tiles).then(() => {
@ -109,7 +108,6 @@ function addToScene(item, position?: Vector3) {
url: url, url: url,
name:item.name.value || item.name, name:item.name.value || item.name,
reset2origin:true, reset2origin:true,
debug:false,
}) })
if (position && tiles.options.reset2origin) { if (position && tiles.options.reset2origin) {

View File

@ -246,7 +246,6 @@ function addToScene(asset: IAssets.Item,positionOrObject?: Vector3 | Object3D) {
url: url, url: url,
name: asset.name, name: asset.name,
reset2origin:true, reset2origin:true,
debug:false,
}) })
if(positionOrObject && (positionOrObject as Vector3).isVector3 && tiles.options.reset2origin){ if(positionOrObject && (positionOrObject as Vector3).isVector3 && tiles.options.reset2origin){

View File

@ -125,7 +125,7 @@ function getScene(sceneInfo) {
<n-layout-sider collapse-mode="transform" :collapsed-width="0" :width="siderWidth" <n-layout-sider collapse-mode="transform" :collapsed-width="0" :width="siderWidth"
:native-scrollbar="false" show-trigger="bar" bordered> :native-scrollbar="false" show-trigger="bar" bordered>
<Layout.Sider/> <Layout.Sidebar/>
</n-layout-sider> </n-layout-sider>
</n-layout> </n-layout>

View File

@ -17,7 +17,8 @@ import {
Opacity, Opacity,
ImageReference, ImageReference,
LocationHeart, LocationHeart,
LocationCompany LocationCompany,
ChoroplethMap
} from "@vicons/carbon"; } from "@vicons/carbon";
import { t } from "@/language"; import { t } from "@/language";
@ -30,13 +31,13 @@ import SidebarHistory from "./sidebar/SidebarHistory.vue";
import SidebarObject from "./sidebar/SidebarObject.vue"; import SidebarObject from "./sidebar/SidebarObject.vue";
import SidebarGeometry from "./sidebar/SidebarGeometry.vue"; import SidebarGeometry from "./sidebar/SidebarGeometry.vue";
import SidebarMaterial from "./sidebar/SidebarMaterial.vue"; import SidebarMaterial from "./sidebar/SidebarMaterial.vue";
// import SidebarSceneTheme from "./sidebar/SidebarSceneTheme.vue";
import SidebarAnimations from "./sidebar/SidebarAnimations.vue"; import SidebarAnimations from "./sidebar/SidebarAnimations.vue";
import SidebarScript from "./sidebar/SidebarScript.vue"; import SidebarScript from "./sidebar/SidebarScript.vue";
import SidebarDrawing from "./sidebar/SidebarDrawing.vue"; import SidebarDrawing from "./sidebar/SidebarDrawing.vue";
import SidebarParticle from "./sidebar/SidebarParticle.vue"; import SidebarParticle from "./sidebar/SidebarParticle.vue";
import SidebarBillboard from "./sidebar/SidebarBillboard.vue"; import SidebarBillboard from "./sidebar/SidebarBillboard.vue";
import SidebarHtmlPanel from "./sidebar/SidebarHtmlPanel.vue"; import SidebarHtmlPanel from "./sidebar/SidebarHtmlPanel.vue";
import Sidebar3DTiles from "./sidebar/Sidebar3DTiles.vue";
const tabsInstRef = ref<TabsInst | null>(null); const tabsInstRef = ref<TabsInst | null>(null);
const tabs = ref<Array<any>>([]); const tabs = ref<Array<any>>([]);
@ -58,7 +59,6 @@ function setTabs(object){
{ name: "renderer", icon: { text: 'Renderer config',color:"#A9A9A9", component: markRaw(ImageReference) }, component: markRaw(SidebarRenderer) }, { name: "renderer", icon: { text: 'Renderer config',color:"#A9A9A9", component: markRaw(ImageReference) }, component: markRaw(SidebarRenderer) },
{ name: "effect", icon: { text: 'Post processing',color:"#A9A9A9", component: markRaw(MagicWandFilled) }, component: markRaw(SidebarEffect) }, { name: "effect", icon: { text: 'Post processing',color:"#A9A9A9", component: markRaw(MagicWandFilled) }, component: markRaw(SidebarEffect) },
{ name: "weather", icon: { text: 'Weather',color:"#A9A9A9", component: markRaw(CloudSnow) }, component: markRaw(SidebarWeather) }, { name: "weather", icon: { text: 'Weather',color:"#A9A9A9", component: markRaw(CloudSnow) }, component: markRaw(SidebarWeather) },
// {name:"styles",icon:{text:'Scene theme',color:"#A9A9A9",component:markRaw(JoinOuter)},component:markRaw(SidebarSceneTheme)},
{ name: "history", icon: { text: 'History',color:"#A9A9A9", component: markRaw(ResultOld) }, component: markRaw(SidebarHistory) }, { name: "history", icon: { text: 'History',color:"#A9A9A9", component: markRaw(ResultOld) }, component: markRaw(SidebarHistory) },
{ name: "drawing", icon: { text: 'Scene drawing',color:"#A9A9A9", component: markRaw(Image) }, component: markRaw(SidebarDrawing) }, { name: "drawing", icon: { text: 'Scene drawing',color:"#A9A9A9", component: markRaw(Image) }, component: markRaw(SidebarDrawing) },
]; ];
@ -91,6 +91,11 @@ function setTabs(object){
current.value = 'htmlPanel'; current.value = 'htmlPanel';
break; break;
case "TilesGroup":
object3DTabs.push({ name: '3DTiles', icon: { text: '3D Tiles',color:"#A9575F", component: markRaw(ChoroplethMap) }, component: markRaw(Sidebar3DTiles) })
current.value = '3DTiles';
break;
} }
} }
@ -118,6 +123,13 @@ function setTabs(object){
current.value = 'object'; current.value = 'object';
} }
} }
// 3DTiles3DTiles
if (current.value === '3DTiles') {
if (!Utils.is3DTilesObject(object)){
current.value = 'object';
}
}
} }
onMounted(()=>{ onMounted(()=>{

View File

@ -2,7 +2,7 @@ import Header from "./Header.vue";
import Footer from "./Footer.vue"; import Footer from "./Footer.vue";
import Scene from "./Scene.vue"; import Scene from "./Scene.vue";
// import Cesium from "./Cesium.vue"; // import Cesium from "./Cesium.vue";
import Sider from "./Sider.vue"; import Sidebar from "./Sidebar.vue";
import Assets from "./Assets.vue"; import Assets from "./Assets.vue";
export { Header, Footer, Scene,Sider,Assets }; export { Header, Footer, Scene,Sidebar,Assets };

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, reactive} from "vue";
import {CaretForwardOutline} from "@vicons/ionicons5";
import {App,Utils,Hooks,getDefault3DTilesOptions} from "@astral3d/engine";
import {t} from "@/language";
import Debug from "@/views/editor/layouts/sidebar/tiles/Sidebar.3DTiles.Debug.vue";
const tilesData = reactive(getDefault3DTilesOptions());
function updateUI() {
const object = App.selected;
if(!object) return;
if (!Utils.is3DTilesObject(object)) return;
Utils.deepAssign(tilesData, object.options);
}
function update(key:string): void {
const object = App.selected;
if(!object) return;
if (!Utils.is3DTilesObject(object)) return;
switch (key) {
case "debug":
object.setDebug(tilesData.debug);
break;
}
}
onMounted(() => {
Hooks.useAddSignal("objectSelected", updateUI);
updateUI();
})
onBeforeUnmount(() => {
Hooks.useRemoveSignal("objectSelected", updateUI);
})
</script>
<template>
<n-collapse display-directive="show" :default-expanded-names="['debug']">
<template #arrow>
<n-icon>
<CaretForwardOutline />
</n-icon>
</template>
<!-- Debug -->
<n-collapse-item :title="t('other.Debug')" name="debug">
<Debug :data="tilesData.debug" @update="update" />
</n-collapse-item>
</n-collapse>
</template>
<style scoped lang="less">
</style>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import {t} from "@/language";
import {getDefault3DTilesOptions,TILES_DEBUG_COLOR_MODE} from "@astral3d/engine";
withDefaults(defineProps<{
data: {
enabled: boolean,
colorMode: string,
displayBoxBounds:boolean,
displaySphereBounds:boolean,
}
}>(), {
data: () => getDefault3DTilesOptions().debug
})
const emits = defineEmits(['update']);
function update(){
emits('update', "debug");
}
</script>
<template>
<div class="sidebar-config-item">
<span>{{ t(`other.Enable`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="data.enabled" @update:checked="update()"/>
</div>
</div>
<!-- 颜色模式 -->
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.tiles.Color mode`) }}</span>
<n-select v-model:value="data.colorMode" size="small" :options="[
{label: 'None', value: TILES_DEBUG_COLOR_MODE.None },
{label: 'Screen error', value: TILES_DEBUG_COLOR_MODE['Screen error']},
{label: 'Geometric error', value: TILES_DEBUG_COLOR_MODE['Geometric error']},
{label: 'Distance', value: TILES_DEBUG_COLOR_MODE.Distance},
{label: 'Depth', value: TILES_DEBUG_COLOR_MODE.Depth},
{label: 'Relative depth', value: TILES_DEBUG_COLOR_MODE['Relative depth']},
{label: 'Is leaf', value: TILES_DEBUG_COLOR_MODE['Is leaf']},
{label: 'Random color', value: TILES_DEBUG_COLOR_MODE['Random color']},
{label: 'Random node color', value: TILES_DEBUG_COLOR_MODE['Random node color']},
{label: 'Load order', value: TILES_DEBUG_COLOR_MODE['Load order']},
]" :disabled="!data.enabled" @update:value="update()" />
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.tiles.Box bounds`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="data.displayBoxBounds" @update:checked="update()" :disabled="!data.enabled"/>
</div>
</div>
<div class="sidebar-config-item">
<span>{{ t(`layout.sider.tiles.Sphere bounds`) }}</span>
<div>
<n-checkbox size="small" v-model:checked="data.displaySphereBounds" @update:checked="update()" :disabled="!data.enabled"/>
</div>
</div>
</template>
<style scoped lang="less">
</style>

11
packages/editor/types/three.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
/**
* three.js类型声明
*/
import * as THREE from 'three';
declare module 'three' {
interface Object3D extends THREE.Object3D{
/* 强制解决three本身类型声明产生的各种问题 */
[key:string]:any;
}
}

View File

@ -40,3 +40,19 @@ export const ROAMING_CHARACTERS = {
X_BOT: "X_Bot", X_BOT: "X_Bot",
Y_BOT: "Y_Bot", Y_BOT: "Y_Bot",
} }
/**
* 3DTiles DebugTilesPlugin ColorMode enum
*/
export const TILES_DEBUG_COLOR_MODE = {
"None": 0,
"Screen error": 1,
"Geometric error": 2,
"Distance": 3,
"Depth": 4,
"Relative depth": 5,
"Is leaf": 6,
"Random color": 7,
"Random node color": 8,
"Load order": 10,
}

View File

@ -3,25 +3,34 @@ import {GLTFExtensionsPlugin,GLTFMeshFeaturesExtension,GLTFStructuralMetadataExt
import Loader from "@/core/loader/Loader.ts"; import Loader from "@/core/loader/Loader.ts";
import {PerspectiveCamera, WebGLRenderer, Group, JSONMeta} from "three"; import {PerspectiveCamera, WebGLRenderer, Group, JSONMeta} from "three";
import {deepAssign} from "@/utils"; import {deepAssign} from "@/utils";
import {TILES_DEBUG_COLOR_MODE} from "@/constant";
import {useDispatchSignal} from "@/hooks";
export const getDefault3DTilesOptions = ():ITiles.options => ({
url:"",
reset2origin:true,
debug: {
enabled:false,
colorMode: TILES_DEBUG_COLOR_MODE["Screen error"],
displayBoxBounds: true,
displaySphereBounds: false,
},
name:"Tiles",
errorTarget: 6,
LRUCache:{
maxSize: 4000,
minSize: 3000,
maxBytesSize: 0.4 * 2**30,
minBytesSize: 0.3 * 2**30,
}
})
export default class Tiles extends Group{ export default class Tiles extends Group{
type = "TilesGroup"; type = "TilesGroup";
isTilesGroup = true; isTilesGroup = true;
// 默认配置 // 默认配置
options: ITiles.options = { options: ITiles.options = getDefault3DTilesOptions();
url:"",
reset2origin:true,
debug:false,
name:"Tiles",
errorTarget: 6,
LRUCache:{
maxSize: 4000,
minSize: 3000,
maxBytesSize: 0.4 * 2**30,
minBytesSize: 0.3 * 2**30,
}
};
renderer: TilesRenderer; renderer: TilesRenderer;
@ -37,6 +46,14 @@ export default class Tiles extends Group{
this.name = this.options.name as string; this.name = this.options.name as string;
this.renderer = this.initRenderer(); this.renderer = this.initRenderer();
// 设置debug插件
this.setDebug(this.options.debug,false);
// 瓦片渐显隐
this.renderer.registerPlugin(new TilesFadePlugin());
// 从gpu卸载不可见瓦片数据cpu上仍然存在
this.renderer.registerPlugin(new UnloadTilesPlugin());
this.add(this.renderer.group); this.add(this.renderer.group);
} }
@ -76,19 +93,6 @@ export default class Tiles extends Group{
// tilesRenderer.manager.addHandler( /\.(gltf|glb)$/g, loader ); // tilesRenderer.manager.addHandler( /\.(gltf|glb)$/g, loader );
// }) // })
// 瓦片渐显隐
tilesRenderer.registerPlugin(new TilesFadePlugin());
// 从gpu卸载不可见瓦片数据cpu上仍然存在
tilesRenderer.registerPlugin(new UnloadTilesPlugin());
if(this.options.debug){
// 注册调试插件
tilesRenderer.registerPlugin(new DebugTilesPlugin());
// 获取调试插件
const debugTilesPlugin = tilesRenderer.getPluginByName('DEBUG_TILES_PLUGIN') as DebugTilesPlugin;
// 显示包围盒的线框
debugTilesPlugin.displayBoxBounds = true;
}
// 子级瓦片加载 // 子级瓦片加载
tilesRenderer.addEventListener('load-model', (e) => { tilesRenderer.addEventListener('load-model', (e) => {
e.scene.traverse(c => { e.scene.traverse(c => {
@ -119,6 +123,39 @@ export default class Tiles extends Group{
this.renderer.setResolutionFromRenderer(camera, renderer); this.renderer.setResolutionFromRenderer(camera, renderer);
} }
/**
* debug插件
*/
setDebug(debugOptions:ITiles.options["debug"], needCreate:boolean = true){
deepAssign(this.options.debug, debugOptions);
if(!this.options.debug) return;
// 获取调试插件
let debugTilesPlugin:DebugTilesPlugin | null = this.renderer.getPluginByName('DEBUG_TILES_PLUGIN') as DebugTilesPlugin | null;
if(!debugTilesPlugin){
if(!needCreate) return;
// 注册调试插件
this.renderer.registerPlugin(new DebugTilesPlugin());
// 获取debug插件
debugTilesPlugin = this.renderer.getPluginByName('DEBUG_TILES_PLUGIN') as DebugTilesPlugin;
}
debugTilesPlugin.enabled = this.options.debug.enabled;
debugTilesPlugin.colorMode = this.options.debug.colorMode;
debugTilesPlugin.displayBoxBounds = this.options.debug.displayBoxBounds;
debugTilesPlugin.displaySphereBounds = this.options.debug.displaySphereBounds;
if(!needCreate) return;
// 发起渲染
this.update();
useDispatchSignal("sceneGraphChanged");
}
/** /**
* clone方法 * clone方法
*/ */

View File

@ -1,4 +1,4 @@
export {default as Billboard, getDefaultBillboardOptions} from "./Billboard"; export {default as Billboard, getDefaultBillboardOptions} from "./Billboard";
export {HtmlPanelConverter, HtmlPanel, HtmlSprite} from "./HtmlPanel"; export {HtmlPanelConverter, HtmlPanel, HtmlSprite} from "./HtmlPanel";
export {default as ParticleEmitter, getDefaultParticleConfig} from "./ParticleEmitter"; export {default as ParticleEmitter, getDefaultParticleConfig} from "./ParticleEmitter";
export {default as Tiles} from "./Tile.ts"; export {default as Tiles, getDefault3DTilesOptions} from "./Tile.ts";

View File

@ -330,7 +330,6 @@ export default class Preview extends THREE.EventDispatcher<PreviewerEventMap> {
url: fileOrUrl, url: fileOrUrl,
name: "AstralPreviewTiles", name: "AstralPreviewTiles",
reset2origin:true, reset2origin:true,
debug:false,
}) })
this.addTiles(tiles).then(() => { this.addTiles(tiles).then(() => {
this.modules.controls.fitToBox(tiles,true); this.modules.controls.fitToBox(tiles,true);

View File

@ -104,6 +104,13 @@ export function isHtmlPanelObject(object:Object3D | null){
return object && (object.isHtmlPanel || object.isHtmlSprite) && object.element; return object && (object.isHtmlPanel || object.isHtmlSprite) && object.element;
} }
/**
* 3DTiles对象
*/
export function is3DTilesObject(object:Object3D | null){
return object && (object.isTilesGroup || object.type === "TilesGroup") && object.options;
}
/** /**
* / * /
*/ */

View File

@ -6,8 +6,17 @@ declare namespace ITiles{
name?:string; name?:string;
// 加载的3d tiles由于坐标原因一般不在原点。此配置项为true则重置回原点 // 加载的3d tiles由于坐标原因一般不在原点。此配置项为true则重置回原点
reset2origin?:boolean; reset2origin?:boolean;
// 是否开启tiles的debug模式 // debug模式
debug?:boolean; debug?: {
// 是否启用
enabled:boolean;
// 渲染tile set时使用的颜色模式
colorMode: number;
// 显示包围盒的线框
displayBoxBounds: boolean;
// 显示包围球的线框
displaySphereBounds: boolean;
};
// 目标屏幕空间误差(以像素为单位) // 目标屏幕空间误差(以像素为单位)
errorTarget?: number; errorTarget?: number;
// LRU缓存:TilesRenderer 的实用程序类,用于跟踪当前使用的项目,以便渲染的项目不会被卸载 // LRU缓存:TilesRenderer 的实用程序类,用于跟踪当前使用的项目,以便渲染的项目不会被卸载