Scene3D.vue 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113
  1. <script setup lang="ts">
  2. import { onMounted, onUnmounted, ref, reactive, watch } from 'vue'
  3. import * as THREE from 'three'
  4. import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
  5. import { Sky } from 'three/examples/jsm/objects/Sky.js'
  6. import { StylizedWaterMaterial } from '../materials/waterNew'
  7. import { createWaterFoamUEMaterial } from '../materials/waterFoamUE'
  8. import { createWaterFlowMaterial } from '../materials/waterFlow'
  9. import foamTexUrl from '../assets/texture/T_Waterfall_Foam.PNG'
  10. import directionalFoamTexUrl from '../assets/texture/T_Waterfall_Foam_Directional.PNG'
  11. import flowTexUrl from '../assets/texture/FlowTexture/T_FlowTexture_BC.PNG'
  12. import foamMacroTexUrl from '../assets/texture/FlowTexture/T_FoamMacro_BC.PNG'
  13. import maskTexUrl from '../assets/texture/FlowTexture/MASK_003.PNG'
  14. import flowNormalTexUrl from '../assets/texture/FlowTexture/T_FlowTexture_BC_NORM.PNG'
  15. import foamMacroNormalTexUrl from '../assets/texture/FlowTexture/T_FoamMacro_BC_NORM.PNG'
  16. import langGLBUrl from '../assets/lang.glb'
  17. import cscwaterGLBUrl from '../assets/CSCwater.glb'
  18. import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
  19. import { TilesRenderer } from '3d-tiles-renderer'
  20. import { ReorientationPlugin } from '3d-tiles-renderer/plugins'
  21. import WaterLevelLabel from './WaterLevelLabel.vue'
  22. import {
  23. modelTransformMap as configModelTransformMap,
  24. defaultWaterParams,
  25. defaultCscwaterParams,
  26. defaultFoamMatParams,
  27. defaultFlowParams,
  28. type ModelTransform,
  29. type WaterMaterialParams,
  30. type CscwaterMaterialParams,
  31. type FoamMaterialParams,
  32. type FlowMaterialParams,
  33. sceneLabels,
  34. type WaterLevelLabelConfig,
  35. cameraPresets,
  36. } from '../config/sceneConfig'
  37. // ==================== 外部 Props(嵌入其他项目时使用)====================
  38. const props = withDefaults(defineProps<{
  39. labelValues?: Record<string, number>
  40. showDebugTools?: boolean
  41. tilesetUrl?: string
  42. cameraPosition?: { x: number; y: number; z: number }
  43. cameraTarget?: { x: number; y: number; z: number }
  44. }>(), {
  45. labelValues: () => ({}),
  46. showDebugTools: false,
  47. tilesetUrl: '/scene/tileset.json',
  48. cameraPosition: () => ({ x: -831.56685, y: 40.63456, z: -2225.321 }),
  49. cameraTarget: () => ({ x: -843.0744, y: 12.01539, z: -2182.06814 }),
  50. })
  51. // 继承 3D Tiles 渲染器,修复 asset 字段缺失的兼容性问题
  52. class SuperMapTilesRenderer extends TilesRenderer {
  53. preprocessTileset(json: any, url: string, parent = null) {
  54. if (!json.asset) {
  55. json.asset = { version: '1.0' }
  56. }
  57. const parentProto = Object.getPrototypeOf(Object.getPrototypeOf(this))
  58. parentProto.preprocessTileset.call(this, json, url, parent)
  59. }
  60. }
  61. // 3D 场景挂载点的 DOM 引用
  62. const containerRef = ref<HTMLDivElement>()
  63. // 底部提示消息弹窗
  64. function showToast(msg: string) {
  65. const el = document.createElement('div')
  66. el.className = 'toast-message'
  67. el.textContent = msg
  68. document.body.appendChild(el)
  69. setTimeout(() => {
  70. el.classList.add('toast-fade')
  71. setTimeout(() => el.remove(), 300)
  72. }, 1500)
  73. }
  74. // 鼠标拾取到的坐标
  75. const pickedPosition = ref<{ x: number; y: number; z: number } | null>({ x: 0, y: 0, z: 0 })
  76. // 相机位置/目标/距离等信息
  77. const cameraInfo = ref({
  78. position: { x: 0, y: 0, z: 0 },
  79. target: { x: 0, y: 0, z: 0 },
  80. distance: 0,
  81. minDistance: 0,
  82. maxDistance: 0,
  83. })
  84. // 调试面板的显示开关
  85. const showCoordinatePanel = ref(false)
  86. const showCameraPanel = ref(false)
  87. const showModelPanel = ref(false)
  88. const showMaterialPanel = ref(false)
  89. const showCameraPresetPanel = ref(false)
  90. // ========== 水面材质参数 ==========
  91. const waterParams = ref<WaterMaterialParams>({ ...defaultWaterParams })
  92. // ========== CSCwater 独立材质参数(与主水面分开调节)==========
  93. const cscwaterParams = ref<CscwaterMaterialParams>({ ...defaultCscwaterParams })
  94. // ========== 泡沫片面材质参数 ==========
  95. const foamMatParams = ref<FoamMaterialParams>({ ...defaultFoamMatParams })
  96. // ========== 流动纹理材质参数 ==========
  97. const flowParams = ref<FlowMaterialParams>({ ...defaultFlowParams })
  98. // ---- 从 localStorage 恢复/保存材质默认值 ----
  99. const FLOW_DEFAULTS_KEY = 'flowMatParams_defaults'
  100. function loadFlowDefaults() {
  101. const saved = localStorage.getItem(FLOW_DEFAULTS_KEY)
  102. if (saved) {
  103. try {
  104. const parsed = JSON.parse(saved)
  105. Object.assign(flowParams.value, parsed)
  106. } catch (e) {
  107. console.warn('Failed to parse flow defaults:', e)
  108. }
  109. }
  110. }
  111. function saveFlowDefaults() {
  112. localStorage.setItem(FLOW_DEFAULTS_KEY, JSON.stringify(flowParams.value))
  113. showToast('流动纹理材质默认值已保存')
  114. }
  115. // 将 flow 材质参数同步到着色器的 uniform 变量
  116. function syncFlowParams() {
  117. if (!flowMaterial) return
  118. const p = flowParams.value
  119. flowMaterial.uniforms.uColor.value.set(p.colour)
  120. flowMaterial.uniforms.uSpeedX.value = p.speedX
  121. flowMaterial.uniforms.uSpeedY.value = p.speedY
  122. flowMaterial.uniforms.uTilingU.value = p.tilingU
  123. flowMaterial.uniforms.uTilingV.value = p.tilingV
  124. flowMaterial.uniforms.uRotationAngle.value = p.rotationAngle
  125. flowMaterial.uniforms.uPower.value = p.power
  126. flowMaterial.uniforms.uWaterGate.value = p.waterGate
  127. flowMaterial.uniforms.uWaterGate02.value = p.waterGate02
  128. flowMaterial.uniforms.uEdgeMaskIntensity.value = p.edgeMaskIntensity
  129. flowMaterial.uniforms.uOpaquePower.value = p.opaquePower
  130. flowMaterial.uniforms.uEdgeFade.value = p.edgeFade
  131. }
  132. // 将 cscwater 材质参数同步到着色器的 uniform 变量
  133. function syncCscwaterParams() {
  134. if (!cscwaterMaterial) return
  135. const p = cscwaterParams.value
  136. cscwaterMaterial.uniforms.alpha.value = p.alpha
  137. cscwaterMaterial.uniforms.flowSpeed.value = p.flowSpeed
  138. cscwaterMaterial.uniforms.flowDirection.value.set(p.flowDirectionX, p.flowDirectionY)
  139. cscwaterMaterial.uniforms.normalRotation.value = p.normalRotation
  140. cscwaterMaterial.uniforms.waveHeight.value = p.waveHeight
  141. cscwaterMaterial.uniforms.shallowColor.value.set(p.waterColor)
  142. cscwaterMaterial.uniforms.deepColor.value.set(p.deepColor)
  143. cscwaterMaterial.uniforms.foamIntensity.value = p.foamIntensity
  144. cscwaterMaterial.uniforms.specIntensity.value = p.specIntensity
  145. cscwaterMaterial.uniforms.specPower.value = p.specPower
  146. cscwaterMaterial.uniforms.fresnelPower.value = p.fresnelPower
  147. cscwaterMaterial.uniforms.fresnelIntensity.value = p.fresnelIntensity
  148. cscwaterMaterial.uniforms.depthRange.value = p.depthRange
  149. cscwaterMaterial.uniforms.waterNormalStrength.value = p.waterNormalStrength
  150. cscwaterMaterial.uniforms.waterNormalTiling.value = p.waterNormalTiling
  151. cscwaterMaterial.uniforms.collisionFoamThreshold.value = p.collisionFoamThreshold
  152. cscwaterMaterial.uniforms.collisionFoamStrength.value = p.collisionFoamStrength
  153. }
  154. const FOAM_DEFAULTS_KEY = 'foamMatParams_defaults'
  155. function loadFoamDefaults() {
  156. const saved = localStorage.getItem(FOAM_DEFAULTS_KEY)
  157. if (saved) {
  158. try {
  159. const parsed = JSON.parse(saved)
  160. Object.assign(foamMatParams.value, parsed)
  161. } catch (e) {
  162. console.warn('Failed to parse foam defaults:', e)
  163. }
  164. }
  165. }
  166. function saveFoamDefaults() {
  167. localStorage.setItem(FOAM_DEFAULTS_KEY, JSON.stringify(foamMatParams.value))
  168. showToast('泡沫材质默认值已保存')
  169. }
  170. const WATER_DEFAULTS_KEY = 'waterParams_defaults'
  171. function loadWaterDefaults() {
  172. const saved = localStorage.getItem(WATER_DEFAULTS_KEY)
  173. if (saved) {
  174. try {
  175. const parsed = JSON.parse(saved)
  176. Object.assign(waterParams.value, parsed)
  177. } catch (e) {
  178. console.warn('Failed to parse water defaults:', e)
  179. }
  180. }
  181. }
  182. function saveWaterDefaults() {
  183. localStorage.setItem(WATER_DEFAULTS_KEY, JSON.stringify(waterParams.value))
  184. showToast('水面材质默认值已保存')
  185. }
  186. const CSCWATER_DEFAULTS_KEY = 'cscwaterParams_defaults'
  187. function loadCscwaterDefaults() {
  188. const saved = localStorage.getItem(CSCWATER_DEFAULTS_KEY)
  189. if (saved) {
  190. try {
  191. const parsed = JSON.parse(saved)
  192. Object.assign(cscwaterParams.value, parsed)
  193. } catch (e) {
  194. console.warn('Failed to parse cscwater defaults:', e)
  195. }
  196. }
  197. }
  198. function saveCscwaterDefaults() {
  199. localStorage.setItem(CSCWATER_DEFAULTS_KEY, JSON.stringify(cscwaterParams.value))
  200. showToast('CSCwater 材质默认值已保存')
  201. }
  202. const MODEL_DEFAULTS_KEY = 'modelTransform_defaults'
  203. function saveModelDefaults() {
  204. const t = modelTransform.value
  205. modelTransformMap[selectedModelKey.value] = {
  206. positionX: t.positionX,
  207. positionY: t.positionY,
  208. positionZ: t.positionZ,
  209. rotationX: t.rotationX,
  210. rotationY: t.rotationY,
  211. rotationZ: t.rotationZ,
  212. scaleX: t.scaleX,
  213. scaleY: t.scaleY,
  214. scaleZ: t.scaleZ,
  215. }
  216. const data: Record<string, typeof modelTransformMap.foam> = {}
  217. for (const key in modelTransformMap) {
  218. data[key] = { ...modelTransformMap[key] }
  219. }
  220. localStorage.setItem(MODEL_DEFAULTS_KEY, JSON.stringify(data))
  221. showToast('模型变换默认值已保存')
  222. }
  223. // 场景中可调试的模型列表,按 key 索引
  224. const modelList: Record<string, THREE.Object3D | null> = {}
  225. const selectedModelKey = ref('foam')
  226. const materialList: Record<string, THREE.Material | null> = {}
  227. const selectedMaterialKey = ref('water')
  228. // 各模型的初始变换参数(位置/旋转/缩放)
  229. const modelTransformMap: Record<string, ModelTransform> = {}
  230. for (const key of Object.keys(configModelTransformMap)) {
  231. modelTransformMap[key] = { ...configModelTransformMap[key] }
  232. }
  233. // 当前选中的模型变换值(双向绑定)
  234. const modelTransform = ref({ ...modelTransformMap.foam })
  235. // 将 modelTransform 的数值应用到 Three.js 物体上
  236. function applyModelTransform(key: string) {
  237. const obj = modelList[key]
  238. if (!obj) return
  239. const t = modelTransform.value
  240. if (key === 'foam' && 'isMesh' in obj) {
  241. const mesh = obj as THREE.Mesh
  242. mesh.position.set(t.positionX, t.positionY, t.positionZ)
  243. mesh.rotation.order = 'YXZ'
  244. mesh.rotation.set(
  245. THREE.MathUtils.degToRad(t.rotationX),
  246. THREE.MathUtils.degToRad(t.rotationY),
  247. THREE.MathUtils.degToRad(t.rotationZ)
  248. )
  249. if (mesh.geometry.type === 'PlaneGeometry') {
  250. const geom = mesh.geometry as THREE.PlaneGeometry
  251. geom.dispose()
  252. mesh.geometry = new THREE.PlaneGeometry(t.scaleX, t.scaleY)
  253. }
  254. } else if (key === 'water') {
  255. obj.position.set(t.positionX, t.positionY, t.positionZ)
  256. obj.rotation.order = 'YXZ'
  257. obj.rotation.set(
  258. THREE.MathUtils.degToRad(t.rotationX),
  259. THREE.MathUtils.degToRad(t.rotationY),
  260. THREE.MathUtils.degToRad(t.rotationZ)
  261. )
  262. } else if (key === 'water2') {
  263. obj.position.set(t.positionX, t.positionY, t.positionZ)
  264. obj.rotation.order = 'YXZ'
  265. obj.rotation.set(
  266. THREE.MathUtils.degToRad(t.rotationX),
  267. THREE.MathUtils.degToRad(t.rotationY),
  268. THREE.MathUtils.degToRad(t.rotationZ)
  269. )
  270. } else if (key === 'flow') {
  271. obj.position.set(t.positionX, t.positionY, t.positionZ)
  272. obj.rotation.order = 'YXZ'
  273. obj.rotation.set(
  274. THREE.MathUtils.degToRad(t.rotationX),
  275. THREE.MathUtils.degToRad(t.rotationY),
  276. THREE.MathUtils.degToRad(t.rotationZ)
  277. )
  278. obj.scale.set(t.scaleX, t.scaleY, t.scaleZ)
  279. } else if (key === 'cscwater') {
  280. obj.position.set(t.positionX, t.positionY, t.positionZ)
  281. obj.rotation.order = 'YXZ'
  282. obj.rotation.set(
  283. THREE.MathUtils.degToRad(t.rotationX),
  284. THREE.MathUtils.degToRad(t.rotationY),
  285. THREE.MathUtils.degToRad(t.rotationZ)
  286. )
  287. obj.scale.set(t.scaleX, t.scaleY, t.scaleZ)
  288. }
  289. }
  290. // 切换模型时,加载该模型保存的变换参数
  291. watch(selectedModelKey, (key) => {
  292. const saved = modelTransformMap[key]
  293. if (saved) {
  294. modelTransform.value = { ...saved }
  295. }
  296. })
  297. // 变换参数变化时实时应用到场景
  298. watch(modelTransform, () => {
  299. applyModelTransform(selectedModelKey.value)
  300. }, { deep: true })
  301. // 水位标签
  302. interface LabelData {
  303. config: WaterLevelLabelConfig
  304. componentRef: ReturnType<typeof ref<InstanceType<typeof WaterLevelLabel> | null>>
  305. }
  306. const labelDataList: LabelData[] = []
  307. const labelValues = reactive<Record<string, number>>({})
  308. function initLabelData() {
  309. labelDataList.length = 0
  310. for (const cfg of sceneLabels.main) {
  311. const externalValue = props.labelValues[cfg.id]
  312. labelValues[cfg.id] = externalValue !== undefined ? externalValue : cfg.initialValue
  313. labelDataList.push({
  314. config: cfg,
  315. componentRef: ref<InstanceType<typeof WaterLevelLabel> | null>(null),
  316. })
  317. }
  318. }
  319. initLabelData()
  320. // ========== Three.js 核心变量声明 ==========
  321. let scene: THREE.Scene
  322. let camera: THREE.PerspectiveCamera
  323. let renderer: THREE.WebGLRenderer
  324. let controls: OrbitControls
  325. let sky: Sky
  326. let waterMesh: THREE.Mesh
  327. let water2Mesh: THREE.Mesh | null = null
  328. let foamMesh: THREE.Mesh | null = null
  329. let foamMaterial: THREE.ShaderMaterial | null = null
  330. let flowMesh: THREE.Mesh | null = null
  331. let flowMaterial: THREE.ShaderMaterial | null = null
  332. let cscwaterModel: THREE.Object3D | null = null
  333. let cscwaterMaterial: THREE.ShaderMaterial | null = null
  334. let sunDirection: THREE.Vector3
  335. let animationId: number
  336. let tilesRenderer: SuperMapTilesRenderer | null = null
  337. let raycaster: THREE.Raycaster
  338. let mouse: THREE.Vector2
  339. let depthRenderTarget: THREE.WebGLRenderTarget
  340. let sceneInitialized = false
  341. // 创建天空背景(含体积云效果)
  342. function createSky() {
  343. sky = new Sky()
  344. sky.scale.setScalar(50000)
  345. scene.add(sky)
  346. const uniforms = sky.material.uniforms
  347. uniforms.turbidity.value = 6
  348. uniforms.rayleigh.value = 0.1
  349. uniforms.mieCoefficient.value = 0.005
  350. uniforms.mieDirectionalG.value = 0.7
  351. uniforms.sunPosition.value.set(20, 30, 10)
  352. uniforms.cloudScale.value = 0.0008
  353. uniforms.cloudSpeed.value = 0.00015
  354. uniforms.cloudCoverage.value = 0.6
  355. uniforms.cloudDensity.value = 0.6
  356. uniforms.cloudElevation.value = 0.9
  357. }
  358. // 创建水面网格(使用自定义风格化水材质)
  359. function createWaterSurface() {
  360. const t = modelTransformMap.water
  361. waterMesh = new THREE.Mesh(
  362. new THREE.PlaneGeometry(t.scaleX, t.scaleY, 120, 120),
  363. StylizedWaterMaterial
  364. )
  365. waterMesh.rotation.order = 'YXZ'
  366. waterMesh.rotation.set(
  367. THREE.MathUtils.degToRad(t.rotationX),
  368. THREE.MathUtils.degToRad(t.rotationY),
  369. THREE.MathUtils.degToRad(t.rotationZ)
  370. )
  371. waterMesh.position.set(t.positionX, t.positionY, t.positionZ)
  372. waterMesh.receiveShadow = true
  373. waterMesh.name = 'water'
  374. waterMesh.renderOrder = 0
  375. scene.add(waterMesh)
  376. modelList['water'] = waterMesh
  377. materialList['water'] = StylizedWaterMaterial
  378. StylizedWaterMaterial.uniforms.cameraPos.value.copy(camera.position)
  379. StylizedWaterMaterial.uniforms.sunDirection.value.copy(sunDirection)
  380. StylizedWaterMaterial.uniforms.cameraNear.value = camera.near
  381. StylizedWaterMaterial.uniforms.cameraFar.value = camera.far
  382. const container = containerRef.value!
  383. StylizedWaterMaterial.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight)
  384. }
  385. // 创建第二片面(使用与主水面相同的风格化水材质)
  386. function createWater2Surface() {
  387. const t = modelTransformMap.water2
  388. water2Mesh = new THREE.Mesh(
  389. new THREE.PlaneGeometry(t.scaleX, t.scaleY, 120, 120),
  390. StylizedWaterMaterial
  391. )
  392. water2Mesh.rotation.order = 'YXZ'
  393. water2Mesh.rotation.set(
  394. THREE.MathUtils.degToRad(t.rotationX),
  395. THREE.MathUtils.degToRad(t.rotationY),
  396. THREE.MathUtils.degToRad(t.rotationZ)
  397. )
  398. water2Mesh.position.set(t.positionX, t.positionY, t.positionZ)
  399. water2Mesh.receiveShadow = true
  400. water2Mesh.name = 'water2'
  401. water2Mesh.renderOrder = 0
  402. scene.add(water2Mesh)
  403. modelList['water2'] = water2Mesh
  404. }
  405. // 创建泡沫片面(瀑布泡沫效果)
  406. function createWaterFoamSurface() {
  407. const textureLoader = new THREE.TextureLoader()
  408. const foamTexture = textureLoader.load(foamTexUrl)
  409. foamTexture.wrapS = THREE.RepeatWrapping
  410. foamTexture.wrapT = THREE.RepeatWrapping
  411. const directionalFoamTexture = textureLoader.load(directionalFoamTexUrl)
  412. directionalFoamTexture.wrapS = THREE.RepeatWrapping
  413. directionalFoamTexture.wrapT = THREE.RepeatWrapping
  414. foamMaterial = createWaterFoamUEMaterial({
  415. colour: new THREE.Color(foamMatParams.value.colour),
  416. opacity: foamMatParams.value.opacity,
  417. waterfallSpeed: foamMatParams.value.waterfallSpeed,
  418. edgeMaskTiling: foamMatParams.value.edgeMaskTiling,
  419. edgeMaskSpeed: foamMatParams.value.edgeMaskSpeed,
  420. fresnelExponent: foamMatParams.value.fresnelExponent,
  421. directionalFoamIntensity: foamMatParams.value.directionalFoamIntensity,
  422. directionalFoamContrast: foamMatParams.value.directionalFoamContrast,
  423. directionalFoam1Intensity: foamMatParams.value.directionalFoam1Intensity,
  424. directionalFoam2Intensity: foamMatParams.value.directionalFoam2Intensity,
  425. directionalFoam2Tiling: foamMatParams.value.directionalFoam2Tiling,
  426. directionalFoam2Speed: foamMatParams.value.directionalFoam2Speed,
  427. directionalFoam3Intensity: foamMatParams.value.directionalFoam3Intensity,
  428. foamFalloff: foamMatParams.value.foamFalloff,
  429. gradientTop: foamMatParams.value.gradientTop,
  430. gradientBottom: foamMatParams.value.gradientBottom,
  431. gradientPower: foamMatParams.value.gradientPower,
  432. foamTexture,
  433. directionalFoamTexture,
  434. })
  435. const t = modelTransformMap.foam
  436. const geometry = new THREE.PlaneGeometry(t.scaleX, t.scaleY)
  437. foamMesh = new THREE.Mesh(geometry, foamMaterial)
  438. foamMesh.rotation.order = 'YXZ'
  439. foamMesh.rotation.set(
  440. THREE.MathUtils.degToRad(t.rotationX),
  441. THREE.MathUtils.degToRad(t.rotationY),
  442. THREE.MathUtils.degToRad(t.rotationZ)
  443. )
  444. foamMesh.position.set(t.positionX, t.positionY, t.positionZ)
  445. foamMesh.name = 'foam'
  446. foamMesh.renderOrder = 1
  447. scene.add(foamMesh)
  448. modelList['foam'] = foamMesh
  449. materialList['foam'] = foamMaterial
  450. }
  451. // 加载流动水纹理模型(GLB 模型+流动纹理材质)
  452. function loadWaterFlowModel() {
  453. const textureLoader = new THREE.TextureLoader()
  454. const flowTexture = textureLoader.load(flowTexUrl)
  455. flowTexture.wrapS = THREE.RepeatWrapping
  456. flowTexture.wrapT = THREE.RepeatWrapping
  457. const foamMacroTexture = textureLoader.load(foamMacroTexUrl)
  458. foamMacroTexture.wrapS = THREE.RepeatWrapping
  459. foamMacroTexture.wrapT = THREE.RepeatWrapping
  460. const maskTexture = textureLoader.load(maskTexUrl)
  461. maskTexture.wrapS = THREE.RepeatWrapping
  462. maskTexture.wrapT = THREE.RepeatWrapping
  463. const flowNormalTexture = textureLoader.load(flowNormalTexUrl)
  464. flowNormalTexture.wrapS = THREE.RepeatWrapping
  465. flowNormalTexture.wrapT = THREE.RepeatWrapping
  466. const foamMacroNormalTexture = textureLoader.load(foamMacroNormalTexUrl)
  467. foamMacroNormalTexture.wrapS = THREE.RepeatWrapping
  468. foamMacroNormalTexture.wrapT = THREE.RepeatWrapping
  469. flowMaterial = createWaterFlowMaterial({
  470. flowTexture,
  471. foamMacroTexture,
  472. maskTexture,
  473. flowNormalTexture,
  474. foamMacroNormalTexture,
  475. })
  476. syncFlowParams()
  477. const loader = new GLTFLoader()
  478. loader.load(langGLBUrl, (gltf) => {
  479. const object = gltf.scene
  480. object.traverse((child) => {
  481. if ((child as THREE.Mesh).isMesh) {
  482. const mesh = child as THREE.Mesh
  483. mesh.material = flowMaterial!
  484. mesh.castShadow = true
  485. mesh.receiveShadow = true
  486. mesh.frustumCulled = false
  487. mesh.renderOrder = 1
  488. flowMesh = mesh
  489. }
  490. })
  491. const t = modelTransformMap.flow
  492. object.position.set(t.positionX, t.positionY, t.positionZ)
  493. object.rotation.order = 'YXZ'
  494. object.rotation.set(
  495. THREE.MathUtils.degToRad(t.rotationX),
  496. THREE.MathUtils.degToRad(t.rotationY),
  497. THREE.MathUtils.degToRad(t.rotationZ)
  498. )
  499. object.scale.set(t.scaleX, t.scaleY, t.scaleZ)
  500. object.name = 'flow'
  501. scene.add(object)
  502. modelList['flow'] = object
  503. materialList['flow'] = flowMaterial
  504. })
  505. }
  506. // 加载 CSCwater.glb 模型
  507. function loadCSCWaterModel() {
  508. cscwaterMaterial = StylizedWaterMaterial.clone()
  509. cscwaterMaterial.uniforms = THREE.UniformsUtils.clone(StylizedWaterMaterial.uniforms)
  510. syncCscwaterParams()
  511. const loader = new GLTFLoader()
  512. loader.load(cscwaterGLBUrl, (gltf) => {
  513. const object = gltf.scene
  514. object.traverse((child) => {
  515. if ((child as THREE.Mesh).isMesh) {
  516. const mesh = child as THREE.Mesh
  517. mesh.material = cscwaterMaterial!
  518. mesh.castShadow = true
  519. mesh.receiveShadow = true
  520. mesh.frustumCulled = false
  521. mesh.renderOrder = 1
  522. }
  523. })
  524. const t = modelTransformMap.cscwater
  525. object.position.set(t.positionX, t.positionY, t.positionZ)
  526. object.rotation.order = 'YXZ'
  527. object.rotation.set(
  528. THREE.MathUtils.degToRad(t.rotationX),
  529. THREE.MathUtils.degToRad(t.rotationY),
  530. THREE.MathUtils.degToRad(t.rotationZ)
  531. )
  532. object.scale.set(t.scaleX, t.scaleY, t.scaleZ)
  533. object.name = 'cscwater'
  534. scene.add(object)
  535. modelList['cscwater'] = object
  536. cscwaterModel = object
  537. materialList['cscwater'] = cscwaterMaterial
  538. })
  539. }
  540. // 加载超图 3D Tiles 瓦片数据(大范围三维场景)
  541. async function load3DTiles() {
  542. const tilesetUrl = props.tilesetUrl
  543. tilesRenderer = new SuperMapTilesRenderer(tilesetUrl)
  544. tilesRenderer.setCamera(camera)
  545. tilesRenderer.setResolutionFromRenderer(camera, renderer)
  546. tilesRenderer.registerPlugin(new ReorientationPlugin())
  547. scene.add(tilesRenderer.group)
  548. scene.fog = null
  549. tilesRenderer.addEventListener('load-error', (evt) => {
  550. console.error('3D Tiles load error:', evt.error, 'URL:', evt.url)
  551. })
  552. tilesRenderer.addEventListener('load-tile', (evt) => {
  553. console.log('Tile loaded:', evt.tile.content?.uri)
  554. })
  555. tilesRenderer.addEventListener('error-tile', (evt) => {
  556. console.error('Tile error:', evt.tile.content?.uri, evt.error)
  557. })
  558. console.log('3D Tiles renderer initialized:', tilesetUrl)
  559. }
  560. // 释放所有 Three.js 资源(可在 initScene 之前和 onUnmounted 时反复调用)
  561. function disposeScene() {
  562. cancelAnimationFrame(animationId)
  563. animationId = 0
  564. labelDataList.forEach(d => d.componentRef.value?.dispose())
  565. if (tilesRenderer) {
  566. if (scene) scene.remove(tilesRenderer.group)
  567. tilesRenderer.dispose?.()
  568. tilesRenderer = null
  569. }
  570. if (foamMesh) {
  571. if (scene) scene.remove(foamMesh)
  572. foamMesh.geometry.dispose()
  573. if (foamMaterial) {
  574. for (const key of Object.keys(foamMaterial.uniforms)) {
  575. const val = foamMaterial.uniforms[key].value
  576. if (val instanceof THREE.Texture) val.dispose()
  577. }
  578. foamMaterial.dispose()
  579. }
  580. foamMesh = null
  581. foamMaterial = null
  582. }
  583. if (flowMesh) {
  584. const parent = flowMesh.parent
  585. if (parent && scene) scene.remove(parent)
  586. flowMesh.geometry.dispose()
  587. if (flowMaterial) {
  588. for (const key of Object.keys(flowMaterial.uniforms)) {
  589. const val = flowMaterial.uniforms[key].value
  590. if (val instanceof THREE.Texture) val.dispose()
  591. }
  592. flowMaterial.dispose()
  593. }
  594. flowMesh = null
  595. flowMaterial = null
  596. }
  597. if (cscwaterModel) {
  598. if (scene) scene.remove(cscwaterModel)
  599. cscwaterModel.traverse((child) => {
  600. const mesh = child as THREE.Mesh
  601. if (mesh.isMesh) {
  602. mesh.geometry?.dispose()
  603. if (Array.isArray(mesh.material)) {
  604. mesh.material.forEach(m => m.dispose())
  605. } else {
  606. mesh.material?.dispose()
  607. }
  608. }
  609. })
  610. cscwaterModel = null
  611. }
  612. if (cscwaterMaterial) {
  613. cscwaterMaterial.dispose()
  614. cscwaterMaterial = null
  615. }
  616. if (waterMesh) {
  617. if (scene) scene.remove(waterMesh)
  618. waterMesh.geometry.dispose()
  619. waterMesh = null
  620. }
  621. if (water2Mesh) {
  622. if (scene) scene.remove(water2Mesh)
  623. water2Mesh.geometry.dispose()
  624. water2Mesh = null
  625. }
  626. if (StylizedWaterMaterial) {
  627. StylizedWaterMaterial.dispose()
  628. }
  629. if (sky) {
  630. if (scene) scene.remove(sky)
  631. sky.material.dispose()
  632. sky = null
  633. }
  634. if (depthRenderTarget) {
  635. depthRenderTarget.depthTexture?.dispose()
  636. depthRenderTarget.dispose()
  637. depthRenderTarget = null
  638. }
  639. if (controls) {
  640. controls.dispose()
  641. controls = null
  642. }
  643. if (renderer) {
  644. const domEl = renderer.domElement
  645. if (domEl && domEl.parentNode) {
  646. domEl.parentNode.removeChild(domEl)
  647. }
  648. renderer.forceContextLoss()
  649. renderer.dispose()
  650. renderer = null
  651. }
  652. if (scene) {
  653. while (scene.children.length > 0) {
  654. scene.remove(scene.children[0])
  655. }
  656. scene = null
  657. }
  658. camera = null
  659. sceneInitialized = false
  660. }
  661. // 初始化整个 Three.js 场景(带防重入保护)
  662. function initScene() {
  663. if (sceneInitialized) {
  664. disposeScene()
  665. }
  666. const container = containerRef.value!
  667. scene = new THREE.Scene()
  668. camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100000)
  669. camera.position.set(props.cameraPosition.x, props.cameraPosition.y, props.cameraPosition.z)
  670. renderer = new THREE.WebGLRenderer({ antialias: true })
  671. renderer.setSize(container.clientWidth, container.clientHeight)
  672. renderer.domElement.style.width = '100%'
  673. renderer.domElement.style.height = '100%'
  674. renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  675. renderer.toneMapping = THREE.ACESFilmicToneMapping
  676. renderer.toneMappingExposure = 1.0
  677. renderer.shadowMap.enabled = true
  678. renderer.shadowMap.type = THREE.PCFShadowMap
  679. container.appendChild(renderer.domElement)
  680. const pixelWidth = Math.floor(container.clientWidth * window.devicePixelRatio)
  681. const pixelHeight = Math.floor(container.clientHeight * window.devicePixelRatio)
  682. depthRenderTarget = new THREE.WebGLRenderTarget(pixelWidth, pixelHeight)
  683. depthRenderTarget.depthTexture = new THREE.DepthTexture(pixelWidth, pixelHeight)
  684. controls = new OrbitControls(camera, renderer.domElement)
  685. controls.enableDamping = true
  686. controls.dampingFactor = 0.03
  687. controls.screenSpacePanning = false
  688. controls.minDistance = 0
  689. controls.maxDistance = Infinity
  690. controls.mouseButtons = {
  691. LEFT: THREE.MOUSE.PAN,
  692. MIDDLE: THREE.MOUSE.DOLLY,
  693. RIGHT: THREE.MOUSE.ROTATE,
  694. }
  695. controls.target.set(props.cameraTarget.x, props.cameraTarget.y, props.cameraTarget.z)
  696. controls.maxPolarAngle = Math.PI / 2.1
  697. controls.minDistance = 5
  698. controls.maxDistance = 500
  699. createSky()
  700. const hemisphereLight = new THREE.HemisphereLight(0xd4d4d4, 0x3d6b4a, 0.6)
  701. scene.add(hemisphereLight)
  702. sunDirection = new THREE.Vector3(20, 30, 10).normalize()
  703. const sunLight = new THREE.DirectionalLight(0xffeedd, 2.0)
  704. sunLight.position.set(20, 30, 10)
  705. sunLight.castShadow = true
  706. sunLight.shadow.mapSize.width = 2048
  707. sunLight.shadow.mapSize.height = 2048
  708. sunLight.shadow.camera.near = 0.5
  709. sunLight.shadow.camera.far = 60
  710. sunLight.shadow.camera.left = -20
  711. sunLight.shadow.camera.right = 20
  712. sunLight.shadow.camera.top = 20
  713. sunLight.shadow.camera.bottom = -20
  714. scene.add(sunLight)
  715. createWaterSurface()
  716. createWater2Surface()
  717. createWaterFoamSurface()
  718. loadWaterFlowModel()
  719. loadCSCWaterModel()
  720. load3DTiles()
  721. labelDataList.forEach(d => d.componentRef.value?.init(scene, camera))
  722. initRaycaster()
  723. sceneInitialized = true
  724. animate()
  725. }
  726. // 初始化鼠标射线拾取
  727. function initRaycaster() {
  728. raycaster = new THREE.Raycaster()
  729. mouse = new THREE.Vector2()
  730. const container = containerRef.value!
  731. container.addEventListener('click', onMouseClick)
  732. }
  733. // 鼠标点击获取 3D 场景中的坐标
  734. function onMouseClick(event: MouseEvent) {
  735. const target = event.target as HTMLElement
  736. if (target.closest('.panel') || target.closest('.toolbar')) return
  737. const container = containerRef.value!
  738. const rect = container.getBoundingClientRect()
  739. mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  740. mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
  741. raycaster.setFromCamera(mouse, camera)
  742. const intersectObjects: THREE.Object3D[] = []
  743. if (tilesRenderer?.group) {
  744. intersectObjects.push(tilesRenderer.group)
  745. }
  746. if (waterMesh) {
  747. intersectObjects.push(waterMesh)
  748. }
  749. const intersects = raycaster.intersectObjects(intersectObjects, true)
  750. if (intersects.length > 0) {
  751. const point = intersects[0].point
  752. pickedPosition.value = {
  753. x: Number(point.x.toFixed(3)),
  754. y: Number(point.y.toFixed(3)),
  755. z: Number(point.z.toFixed(3)),
  756. }
  757. }
  758. }
  759. // 每帧渲染循环
  760. function animate() {
  761. animationId = requestAnimationFrame(animate)
  762. sky.material.uniforms.time.value += 0.001
  763. controls.update()
  764. tilesRenderer?.update()
  765. // 将水面的深度写入深度纹理(用于水面透明交互动效)
  766. waterMesh.visible = false
  767. const prevRT = renderer.getRenderTarget()
  768. renderer.setRenderTarget(depthRenderTarget)
  769. renderer.render(scene, camera)
  770. renderer.setRenderTarget(prevRT)
  771. waterMesh.visible = true
  772. // 更新各材质的时间 uniform 实现动画
  773. StylizedWaterMaterial.uniforms.depthSampler.value = depthRenderTarget.depthTexture
  774. StylizedWaterMaterial.uniforms.iTime.value += 0.016
  775. StylizedWaterMaterial.uniforms.collisionFoamTime.value += 0.016
  776. if (foamMaterial) {
  777. foamMaterial.uniforms.uTime.value += 0.016
  778. foamMaterial.uniforms.uCameraPos.value.copy(camera.position)
  779. }
  780. if (flowMaterial) {
  781. flowMaterial.uniforms.uTime.value += 0.016
  782. }
  783. if (cscwaterMaterial) {
  784. cscwaterMaterial.uniforms.depthSampler.value = depthRenderTarget.depthTexture
  785. cscwaterMaterial.uniforms.iTime.value += 0.016
  786. cscwaterMaterial.uniforms.collisionFoamTime.value += 0.016
  787. cscwaterMaterial.uniforms.cameraPos.value.copy(camera.position)
  788. }
  789. labelDataList.forEach(d => d.componentRef.value?.tick())
  790. StylizedWaterMaterial.uniforms.cameraPos.value.copy(camera.position)
  791. // 更新相机信息面板
  792. cameraInfo.value.position = {
  793. x: Number(camera.position.x.toFixed(3)),
  794. y: Number(camera.position.y.toFixed(3)),
  795. z: Number(camera.position.z.toFixed(3)),
  796. }
  797. cameraInfo.value.target = {
  798. x: Number(controls.target.x.toFixed(3)),
  799. y: Number(controls.target.y.toFixed(3)),
  800. z: Number(controls.target.z.toFixed(3)),
  801. }
  802. cameraInfo.value.distance = Number(camera.position.distanceTo(controls.target).toFixed(3))
  803. cameraInfo.value.minDistance = controls.minDistance
  804. cameraInfo.value.maxDistance = controls.maxDistance
  805. renderer.render(scene, camera)
  806. }
  807. // 窗口大小变化自适应
  808. function onResize() {
  809. if (!containerRef.value) return
  810. const width = containerRef.value.clientWidth
  811. const height = containerRef.value.clientHeight
  812. camera.aspect = width / height
  813. camera.updateProjectionMatrix()
  814. renderer.setSize(width, height)
  815. const pixelWidth = Math.floor(width * window.devicePixelRatio)
  816. const pixelHeight = Math.floor(height * window.devicePixelRatio)
  817. depthRenderTarget.setSize(pixelWidth, pixelHeight)
  818. StylizedWaterMaterial.uniforms.iResolution.value.set(width, height)
  819. if (tilesRenderer) {
  820. tilesRenderer.setResolutionFromRenderer(camera, renderer)
  821. }
  822. }
  823. onMounted(() => {
  824. initScene()
  825. window.addEventListener('resize', onResize)
  826. })
  827. // 组件销毁时释放所有 Three.js 资源
  828. onUnmounted(() => {
  829. window.removeEventListener('resize', onResize)
  830. if (containerRef.value) {
  831. containerRef.value.removeEventListener('click', onMouseClick)
  832. }
  833. disposeScene()
  834. })
  835. // ========== 材质参数的响应式监听,实时同步到着色器 ==========
  836. function syncWaterParams() {
  837. if (!StylizedWaterMaterial || !StylizedWaterMaterial.uniforms) return
  838. const p = waterParams.value
  839. StylizedWaterMaterial.uniforms.alpha.value = p.alpha
  840. StylizedWaterMaterial.uniforms.flowSpeed.value = p.flowSpeed
  841. StylizedWaterMaterial.uniforms.flowDirection.value.set(p.flowDirectionX, p.flowDirectionY)
  842. StylizedWaterMaterial.uniforms.normalRotation.value = p.normalRotation
  843. StylizedWaterMaterial.uniforms.waveHeight.value = p.waveHeight
  844. StylizedWaterMaterial.uniforms.shallowColor.value.set(p.waterColor)
  845. StylizedWaterMaterial.uniforms.deepColor.value.set(p.deepColor)
  846. StylizedWaterMaterial.uniforms.foamIntensity.value = p.foamIntensity
  847. StylizedWaterMaterial.uniforms.specIntensity.value = p.specIntensity
  848. StylizedWaterMaterial.uniforms.specPower.value = p.specPower
  849. StylizedWaterMaterial.uniforms.fresnelPower.value = p.fresnelPower
  850. StylizedWaterMaterial.uniforms.fresnelIntensity.value = p.fresnelIntensity
  851. StylizedWaterMaterial.uniforms.depthRange.value = p.depthRange
  852. StylizedWaterMaterial.uniforms.waterNormalStrength.value = p.waterNormalStrength
  853. StylizedWaterMaterial.uniforms.waterNormalTiling.value = p.waterNormalTiling
  854. StylizedWaterMaterial.uniforms.collisionFoamThreshold.value = p.collisionFoamThreshold
  855. StylizedWaterMaterial.uniforms.collisionFoamStrength.value = p.collisionFoamStrength
  856. }
  857. watch(() => waterParams.value, syncWaterParams, { deep: true })
  858. syncWaterParams()
  859. watch(() => cscwaterParams.value, () => {
  860. syncCscwaterParams()
  861. }, { deep: true })
  862. watch(() => foamMatParams.value, (p) => {
  863. if (!foamMaterial) return
  864. foamMaterial.uniforms.uColour.value.set(p.colour)
  865. foamMaterial.uniforms.uOpacity.value = p.opacity
  866. foamMaterial.uniforms.uWaterfallSpeed.value = p.waterfallSpeed
  867. foamMaterial.uniforms.uEdgeMaskTiling.value = p.edgeMaskTiling
  868. foamMaterial.uniforms.uEdgeMaskSpeed.value = p.edgeMaskSpeed
  869. foamMaterial.uniforms.uFresnelExponent.value = p.fresnelExponent
  870. foamMaterial.uniforms.uDirectionalFoamIntensity.value = p.directionalFoamIntensity
  871. foamMaterial.uniforms.uDirectionalFoamContrast.value = p.directionalFoamContrast
  872. foamMaterial.uniforms.uDirectionalFoam1Intensity.value = p.directionalFoam1Intensity
  873. foamMaterial.uniforms.uDirectionalFoam2Intensity.value = p.directionalFoam2Intensity
  874. foamMaterial.uniforms.uDirectionalFoam2Tiling.value = p.directionalFoam2Tiling
  875. foamMaterial.uniforms.uDirectionalFoam2Speed.value = p.directionalFoam2Speed
  876. foamMaterial.uniforms.uDirectionalFoam3Intensity.value = p.directionalFoam3Intensity
  877. foamMaterial.uniforms.uFoamFalloff.value = p.foamFalloff
  878. foamMaterial.uniforms.uGradientTop.value = p.gradientTop
  879. foamMaterial.uniforms.uGradientBottom.value = p.gradientBottom
  880. foamMaterial.uniforms.uGradientPower.value = p.gradientPower
  881. }, { deep: true })
  882. watch(() => flowParams.value, () => {
  883. syncFlowParams()
  884. }, { deep: true })
  885. watch(() => props.labelValues, (levels) => {
  886. for (const [id, value] of Object.entries(levels)) {
  887. if (labelValues[id] !== undefined) {
  888. labelValues[id] = value
  889. }
  890. }
  891. }, { deep: true })
  892. // ==================== 对外暴露的 API(供父组件/外部系统调用)====================
  893. function setLabelValue(id: string, value: number) {
  894. labelValues[id] = value
  895. }
  896. function setLabelValues(levels: Record<string, number>) {
  897. for (const [id, value] of Object.entries(levels)) {
  898. labelValues[id] = value
  899. }
  900. }
  901. function flyTo(
  902. position: { x: number; y: number; z: number },
  903. target?: { x: number; y: number; z: number },
  904. durationMs: number = 1500,
  905. ) {
  906. const startPos = camera.position.clone()
  907. const endPos = new THREE.Vector3(position.x, position.y, position.z)
  908. const startTarget = controls.target.clone()
  909. const endTarget = target
  910. ? new THREE.Vector3(target.x, target.y, target.z)
  911. : controls.target.clone()
  912. const startTime = performance.now()
  913. function animateFly() {
  914. const elapsed = performance.now() - startTime
  915. const t = Math.min(elapsed / durationMs, 1)
  916. const ease = t * t * (3 - 2 * t)
  917. camera.position.lerpVectors(startPos, endPos, ease)
  918. controls.target.lerpVectors(startTarget, endTarget, ease)
  919. controls.update()
  920. if (t < 1) requestAnimationFrame(animateFly)
  921. }
  922. animateFly()
  923. }
  924. function flyToPreset(presetId: string, durationMs: number = 1500) {
  925. const preset = cameraPresets.find(p => p.id === presetId)
  926. if (!preset) {
  927. console.warn(`Camera preset "${presetId}" not found`)
  928. return
  929. }
  930. flyTo(
  931. { x: preset.positionX, y: preset.positionY, z: preset.positionZ },
  932. { x: preset.targetX, y: preset.targetY, z: preset.targetZ },
  933. durationMs,
  934. )
  935. }
  936. defineExpose({
  937. labelDataList,
  938. labelValues,
  939. setLabelValue,
  940. setLabelValues,
  941. flyTo,
  942. flyToPreset,
  943. cameraPresets,
  944. })
  945. </script>
  946. <template>
  947. <!-- 3D 渲染容器 -->
  948. <div ref="containerRef" class="scene-container" />
  949. <!-- 水位标签(Sprite + 指针) -->
  950. <WaterLevelLabel
  951. v-for="data in labelDataList"
  952. :key="data.config.id"
  953. :ref="(el: any) => { if (el) data.componentRef.value = el }"
  954. :label-id="data.config.id"
  955. :type="data.config.type"
  956. :position-x="data.config.positionX"
  957. :position-y="data.config.positionY"
  958. :position-z="data.config.positionZ"
  959. :value="labelValues[data.config.id]"
  960. />
  961. <!-- 右上角调试工具栏 -->
  962. <div v-if="props.showDebugTools" class="toolbar">
  963. <button class="toolbar-btn" :class="{ active: showCoordinatePanel }" @click="showCoordinatePanel = !showCoordinatePanel">坐标</button>
  964. <button class="toolbar-btn" :class="{ active: showCameraPanel }" @click="showCameraPanel = !showCameraPanel">相机</button>
  965. <button class="toolbar-btn" :class="{ active: showModelPanel }" @click="showModelPanel = !showModelPanel">模型</button>
  966. <button class="toolbar-btn" :class="{ active: showMaterialPanel }" @click="showMaterialPanel = !showMaterialPanel">材质</button>
  967. <button class="toolbar-btn" :class="{ active: showCameraPresetPanel }" @click="showCameraPresetPanel = !showCameraPresetPanel">视角</button>
  968. </div>
  969. <!-- 拾取坐标面板 -->
  970. <div v-if="props.showDebugTools && showCoordinatePanel && pickedPosition" class="panel coordinate-panel">
  971. <div class="panel-header">
  972. <span class="panel-title">拾取坐标 (m)</span>
  973. <button class="toggle-btn" @click="showCoordinatePanel = false">×</button>
  974. </div>
  975. <div class="coordinate-item">
  976. <span class="coordinate-label">X:</span>
  977. <span class="coordinate-value">{{ pickedPosition.x }}</span>
  978. </div>
  979. <div class="coordinate-item">
  980. <span class="coordinate-label">Y:</span>
  981. <span class="coordinate-value">{{ pickedPosition.y }}</span>
  982. </div>
  983. <div class="coordinate-item">
  984. <span class="coordinate-label">Z:</span>
  985. <span class="coordinate-value">{{ pickedPosition.z }}</span>
  986. </div>
  987. </div>
  988. <!-- 相机信息面板 -->
  989. <div v-if="props.showDebugTools && showCameraPanel" class="panel camera-panel">
  990. <div class="panel-header">
  991. <span class="panel-title">相机信息</span>
  992. <button class="toggle-btn" @click="showCameraPanel = false">×</button>
  993. </div>
  994. <div class="camera-section">
  995. <div class="section-label">位置 (m)</div>
  996. <div class="camera-item">
  997. <span class="camera-label">X:</span>
  998. <span class="camera-value">{{ cameraInfo.position.x }}</span>
  999. </div>
  1000. <div class="camera-item">
  1001. <span class="camera-label">Y:</span>
  1002. <span class="camera-value">{{ cameraInfo.position.y }}</span>
  1003. </div>
  1004. <div class="camera-item">
  1005. <span class="camera-label">Z:</span>
  1006. <span class="camera-value">{{ cameraInfo.position.z }}</span>
  1007. </div>
  1008. </div>
  1009. <div class="camera-section">
  1010. <div class="section-label">目标点 (m)</div>
  1011. <div class="camera-item">
  1012. <span class="camera-label">X:</span>
  1013. <span class="camera-value">{{ cameraInfo.target.x }}</span>
  1014. </div>
  1015. <div class="camera-item">
  1016. <span class="camera-label">Y:</span>
  1017. <span class="camera-value">{{ cameraInfo.target.y }}</span>
  1018. </div>
  1019. <div class="camera-item">
  1020. <span class="camera-label">Z:</span>
  1021. <span class="camera-value">{{ cameraInfo.target.z }}</span>
  1022. </div>
  1023. </div>
  1024. <div class="camera-section">
  1025. <div class="section-label">缩放距离 (m)</div>
  1026. <div class="camera-item">
  1027. <span class="camera-label">当前:</span>
  1028. <span class="camera-value">{{ cameraInfo.distance }}</span>
  1029. </div>
  1030. <div class="camera-item">
  1031. <span class="camera-label">最近:</span>
  1032. <span class="camera-value">{{ cameraInfo.minDistance }}m</span>
  1033. </div>
  1034. <div class="camera-item">
  1035. <span class="camera-label">最远:</span>
  1036. <span class="camera-value">{{ cameraInfo.maxDistance }}m</span>
  1037. </div>
  1038. </div>
  1039. </div>
  1040. <!-- 模型变换调试面板 -->
  1041. <div v-if="props.showDebugTools && showModelPanel" class="panel model-panel">
  1042. <div class="panel-header">
  1043. <span class="panel-title">模型变换</span>
  1044. <button class="toggle-btn" @click="showModelPanel = false">×</button>
  1045. </div>
  1046. <div class="model-section">
  1047. <div class="section-label">选择模型</div>
  1048. <select v-model="selectedModelKey" class="model-select">
  1049. <option value="water">水面</option>
  1050. <option value="water2">水面2</option>
  1051. <option value="foam">泡沫片面</option>
  1052. <option value="flow">流动纹理模型</option>
  1053. <option value="cscwater">CSCwater</option>
  1054. </select>
  1055. </div>
  1056. <div class="model-section">
  1057. <div class="section-label">位置</div>
  1058. <div class="input-item">
  1059. <span class="input-label">X</span>
  1060. <input type="number" v-model.number="modelTransform.positionX" step="0.1" class="number-input" />
  1061. </div>
  1062. <div class="input-item">
  1063. <span class="input-label">Y</span>
  1064. <input type="number" v-model.number="modelTransform.positionY" step="0.1" class="number-input" />
  1065. </div>
  1066. <div class="input-item">
  1067. <span class="input-label">Z</span>
  1068. <input type="number" v-model.number="modelTransform.positionZ" step="0.1" class="number-input" />
  1069. </div>
  1070. </div>
  1071. <div class="model-section">
  1072. <div class="section-label">旋转</div>
  1073. <div class="input-item">
  1074. <span class="input-label">X (°)</span>
  1075. <input type="number" v-model.number="modelTransform.rotationX" step="1" class="number-input" />
  1076. </div>
  1077. <div class="input-item">
  1078. <span class="input-label">Y (°)</span>
  1079. <input type="number" v-model.number="modelTransform.rotationY" step="1" class="number-input" />
  1080. </div>
  1081. <div class="input-item">
  1082. <span class="input-label">Z (°)</span>
  1083. <input type="number" v-model.number="modelTransform.rotationZ" step="1" class="number-input" />
  1084. </div>
  1085. </div>
  1086. <div class="model-section">
  1087. <div class="section-label">缩放</div>
  1088. <div class="input-item">
  1089. <span class="input-label">X</span>
  1090. <input type="number" v-model.number="modelTransform.scaleX" step="0.5" class="number-input" />
  1091. </div>
  1092. <div class="input-item">
  1093. <span class="input-label">Y</span>
  1094. <input type="number" v-model.number="modelTransform.scaleY" step="0.5" class="number-input" />
  1095. </div>
  1096. <div class="input-item">
  1097. <span class="input-label">Z</span>
  1098. <input type="number" v-model.number="modelTransform.scaleZ" step="0.5" class="number-input" />
  1099. </div>
  1100. </div>
  1101. <div class="model-section">
  1102. <button class="default-btn" @click="saveModelDefaults">设置默认值</button>
  1103. </div>
  1104. </div>
  1105. <!-- 材质参数调试面板 -->
  1106. <div v-if="props.showDebugTools && showMaterialPanel" class="panel material-panel">
  1107. <div class="panel-header">
  1108. <span class="panel-title">材质参数</span>
  1109. <button class="toggle-btn" @click="showMaterialPanel = false">×</button>
  1110. </div>
  1111. <div class="material-section">
  1112. <div class="section-label">选择材质</div>
  1113. <select v-model="selectedMaterialKey" class="model-select">
  1114. <option value="water">水材质</option>
  1115. <option value="foam">泡沫材质</option>
  1116. <option value="flow">流动纹理材质</option>
  1117. <option value="cscwater">CSCwater 材质</option>
  1118. </select>
  1119. </div>
  1120. <template v-if="selectedMaterialKey === 'water'">
  1121. <div class="material-section">
  1122. <div class="section-label">透明度</div>
  1123. <div class="slider-item">
  1124. <input type="range" v-model.number="waterParams.alpha" min="0" max="1" step="0.01" />
  1125. <span class="slider-value">{{ waterParams.alpha.toFixed(2) }}</span>
  1126. </div>
  1127. </div>
  1128. <div class="material-section">
  1129. <div class="section-label">浪高</div>
  1130. <div class="slider-item">
  1131. <input type="range" v-model.number="waterParams.waveHeight" min="0" max="2" step="0.05" />
  1132. <span class="slider-value">{{ waterParams.waveHeight.toFixed(2) }}</span>
  1133. </div>
  1134. </div>
  1135. <div class="material-section">
  1136. <div class="section-label">水流速度</div>
  1137. <div class="slider-item">
  1138. <input type="range" v-model.number="waterParams.flowSpeed" min="0" max="3" step="0.1" />
  1139. <span class="slider-value">{{ waterParams.flowSpeed.toFixed(1) }}</span>
  1140. </div>
  1141. </div>
  1142. <div class="material-section">
  1143. <div class="section-label">流向 X</div>
  1144. <div class="slider-item">
  1145. <input type="range" v-model.number="waterParams.flowDirectionX" min="-2" max="2" step="0.1" />
  1146. <span class="slider-value">{{ waterParams.flowDirectionX.toFixed(1) }}</span>
  1147. </div>
  1148. </div>
  1149. <div class="material-section">
  1150. <div class="section-label">流向 Z</div>
  1151. <div class="slider-item">
  1152. <input type="range" v-model.number="waterParams.flowDirectionY" min="-2" max="2" step="0.1" />
  1153. <span class="slider-value">{{ waterParams.flowDirectionY.toFixed(1) }}</span>
  1154. </div>
  1155. </div>
  1156. <div class="material-section">
  1157. <div class="section-label">法线旋转</div>
  1158. <div class="slider-item">
  1159. <input type="range" v-model.number="waterParams.normalRotation" min="-180" max="180" step="1" />
  1160. <span class="slider-value">{{ waterParams.normalRotation.toFixed(0) }}°</span>
  1161. </div>
  1162. </div>
  1163. <div class="material-section">
  1164. <div class="section-label">浅水颜色</div>
  1165. <div class="color-item">
  1166. <input type="color" v-model="waterParams.waterColor" />
  1167. <span class="color-value">{{ waterParams.waterColor }}</span>
  1168. </div>
  1169. </div>
  1170. <div class="material-section">
  1171. <div class="section-label">深水颜色</div>
  1172. <div class="color-item">
  1173. <input type="color" v-model="waterParams.deepColor" />
  1174. <span class="color-value">{{ waterParams.deepColor }}</span>
  1175. </div>
  1176. </div>
  1177. <div class="material-section">
  1178. <div class="section-label">水深范围</div>
  1179. <div class="slider-item">
  1180. <input type="range" v-model.number="waterParams.depthRange" min="1" max="50" step="0.5" />
  1181. <span class="slider-value">{{ waterParams.depthRange.toFixed(1) }}</span>
  1182. </div>
  1183. </div>
  1184. <div class="material-section">
  1185. <div class="section-label">水法线强度</div>
  1186. <div class="slider-item">
  1187. <input type="range" v-model.number="waterParams.waterNormalStrength" min="0" max="2" step="0.05" />
  1188. <span class="slider-value">{{ waterParams.waterNormalStrength.toFixed(2) }}</span>
  1189. </div>
  1190. </div>
  1191. <div class="material-section">
  1192. <div class="section-label">水纹平铺</div>
  1193. <div class="slider-item">
  1194. <input type="range" v-model.number="waterParams.waterNormalTiling" min="0.01" max="5" step="0.01" />
  1195. <span class="slider-value">{{ waterParams.waterNormalTiling.toFixed(2) }}</span>
  1196. </div>
  1197. </div>
  1198. <div class="material-section">
  1199. <div class="section-label">高光强度</div>
  1200. <div class="slider-item">
  1201. <input type="range" v-model.number="waterParams.specIntensity" min="0" max="5" step="0.1" />
  1202. <span class="slider-value">{{ waterParams.specIntensity.toFixed(1) }}</span>
  1203. </div>
  1204. </div>
  1205. <div class="material-section">
  1206. <div class="section-label">高光锐度</div>
  1207. <div class="slider-item">
  1208. <input type="range" v-model.number="waterParams.specPower" min="1" max="256" step="1" />
  1209. <span class="slider-value">{{ waterParams.specPower.toFixed(0) }}</span>
  1210. </div>
  1211. </div>
  1212. <div class="material-section">
  1213. <div class="section-label">菲涅尔功率</div>
  1214. <div class="slider-item">
  1215. <input type="range" v-model.number="waterParams.fresnelPower" min="0.1" max="10" step="0.1" />
  1216. <span class="slider-value">{{ waterParams.fresnelPower.toFixed(1) }}</span>
  1217. </div>
  1218. </div>
  1219. <div class="material-section">
  1220. <div class="section-label">菲涅尔强度</div>
  1221. <div class="slider-item">
  1222. <input type="range" v-model.number="waterParams.fresnelIntensity" min="0" max="3" step="0.1" />
  1223. <span class="slider-value">{{ waterParams.fresnelIntensity.toFixed(1) }}</span>
  1224. </div>
  1225. </div>
  1226. <div class="material-section">
  1227. <div class="section-label">泡沫强度</div>
  1228. <div class="slider-item">
  1229. <input type="range" v-model.number="waterParams.foamIntensity" min="0" max="2" step="0.05" />
  1230. <span class="slider-value">{{ waterParams.foamIntensity.toFixed(2) }}</span>
  1231. </div>
  1232. </div>
  1233. <div class="material-section">
  1234. <div class="section-label">碰撞泡沫阈值</div>
  1235. <div class="slider-item">
  1236. <input type="range" v-model.number="waterParams.collisionFoamThreshold" min="0" max="2" step="0.05" />
  1237. <span class="slider-value">{{ waterParams.collisionFoamThreshold.toFixed(2) }}</span>
  1238. </div>
  1239. </div>
  1240. <div class="material-section">
  1241. <div class="section-label">碰撞泡沫强度</div>
  1242. <div class="slider-item">
  1243. <input type="range" v-model.number="waterParams.collisionFoamStrength" min="0" max="3" step="0.1" />
  1244. <span class="slider-value">{{ waterParams.collisionFoamStrength.toFixed(1) }}</span>
  1245. </div>
  1246. </div>
  1247. <div class="material-section">
  1248. <button class="default-btn" @click="saveWaterDefaults">设置默认值</button>
  1249. </div>
  1250. </template>
  1251. <template v-if="selectedMaterialKey === 'foam'">
  1252. <div class="material-section">
  1253. <div class="section-label">颜色</div>
  1254. <div class="color-item">
  1255. <input type="color" v-model="foamMatParams.colour" />
  1256. <span class="color-value">{{ foamMatParams.colour }}</span>
  1257. </div>
  1258. </div>
  1259. <div class="material-section">
  1260. <div class="section-label">不透明度</div>
  1261. <div class="slider-item">
  1262. <input type="range" v-model.number="foamMatParams.opacity" min="0" max="1" step="0.01" />
  1263. <span class="slider-value">{{ foamMatParams.opacity.toFixed(2) }}</span>
  1264. </div>
  1265. </div>
  1266. <div class="material-section">
  1267. <div class="section-label">瀑布流速</div>
  1268. <div class="slider-item">
  1269. <input type="range" v-model.number="foamMatParams.waterfallSpeed" min="0" max="2" step="0.01" />
  1270. <span class="slider-value">{{ foamMatParams.waterfallSpeed.toFixed(2) }}</span>
  1271. </div>
  1272. </div>
  1273. <div class="material-section">
  1274. <div class="section-label">菲涅尔指数</div>
  1275. <div class="slider-item">
  1276. <input type="range" v-model.number="foamMatParams.fresnelExponent" min="0" max="20" step="0.1" />
  1277. <span class="slider-value">{{ foamMatParams.fresnelExponent.toFixed(1) }}</span>
  1278. </div>
  1279. </div>
  1280. <div class="material-section">
  1281. <div class="section-label">边缘遮罩平铺</div>
  1282. <div class="slider-item">
  1283. <input type="range" v-model.number="foamMatParams.edgeMaskTiling" min="0.1" max="2" step="0.01" />
  1284. <span class="slider-value">{{ foamMatParams.edgeMaskTiling.toFixed(2) }}</span>
  1285. </div>
  1286. </div>
  1287. <div class="material-section">
  1288. <div class="section-label">边缘遮罩速度</div>
  1289. <div class="slider-item">
  1290. <input type="range" v-model.number="foamMatParams.edgeMaskSpeed" min="0" max="10" step="0.1" />
  1291. <span class="slider-value">{{ foamMatParams.edgeMaskSpeed.toFixed(1) }}</span>
  1292. </div>
  1293. </div>
  1294. <div class="material-section">
  1295. <div class="section-label">方向泡沫强度</div>
  1296. <div class="slider-item">
  1297. <input type="range" v-model.number="foamMatParams.directionalFoamIntensity" min="0" max="5" step="0.01" />
  1298. <span class="slider-value">{{ foamMatParams.directionalFoamIntensity.toFixed(2) }}</span>
  1299. </div>
  1300. </div>
  1301. <div class="material-section">
  1302. <div class="section-label">方向泡沫对比度</div>
  1303. <div class="slider-item">
  1304. <input type="range" v-model.number="foamMatParams.directionalFoamContrast" min="0.1" max="10" step="0.1" />
  1305. <span class="slider-value">{{ foamMatParams.directionalFoamContrast.toFixed(1) }}</span>
  1306. </div>
  1307. </div>
  1308. <div class="material-section">
  1309. <div class="section-label">层1强度</div>
  1310. <div class="slider-item">
  1311. <input type="range" v-model.number="foamMatParams.directionalFoam1Intensity" min="0" max="5" step="0.01" />
  1312. <span class="slider-value">{{ foamMatParams.directionalFoam1Intensity.toFixed(2) }}</span>
  1313. </div>
  1314. </div>
  1315. <div class="material-section">
  1316. <div class="section-label">层2强度</div>
  1317. <div class="slider-item">
  1318. <input type="range" v-model.number="foamMatParams.directionalFoam2Intensity" min="0" max="5" step="0.01" />
  1319. <span class="slider-value">{{ foamMatParams.directionalFoam2Intensity.toFixed(2) }}</span>
  1320. </div>
  1321. </div>
  1322. <div class="material-section">
  1323. <div class="section-label">层2平铺</div>
  1324. <div class="slider-item">
  1325. <input type="range" v-model.number="foamMatParams.directionalFoam2Tiling" min="0.1" max="5" step="0.01" />
  1326. <span class="slider-value">{{ foamMatParams.directionalFoam2Tiling.toFixed(2) }}</span>
  1327. </div>
  1328. </div>
  1329. <div class="material-section">
  1330. <div class="section-label">层2速度</div>
  1331. <div class="slider-item">
  1332. <input type="range" v-model.number="foamMatParams.directionalFoam2Speed" min="0" max="50" step="0.5" />
  1333. <span class="slider-value">{{ foamMatParams.directionalFoam2Speed.toFixed(1) }}</span>
  1334. </div>
  1335. </div>
  1336. <div class="material-section">
  1337. <div class="section-label">层3强度</div>
  1338. <div class="slider-item">
  1339. <input type="range" v-model.number="foamMatParams.directionalFoam3Intensity" min="0" max="5" step="0.01" />
  1340. <span class="slider-value">{{ foamMatParams.directionalFoam3Intensity.toFixed(2) }}</span>
  1341. </div>
  1342. </div>
  1343. <div class="material-section">
  1344. <div class="section-label">泡沫衰减</div>
  1345. <div class="slider-item">
  1346. <input type="range" v-model.number="foamMatParams.foamFalloff" min="0.1" max="10" step="0.1" />
  1347. <span class="slider-value">{{ foamMatParams.foamFalloff.toFixed(1) }}</span>
  1348. </div>
  1349. </div>
  1350. <div class="material-section">
  1351. <div class="section-label">渐变顶部</div>
  1352. <div class="slider-item">
  1353. <input type="range" v-model.number="foamMatParams.gradientTop" min="0" max="1" step="0.01" />
  1354. <span class="slider-value">{{ foamMatParams.gradientTop.toFixed(2) }}</span>
  1355. </div>
  1356. </div>
  1357. <div class="material-section">
  1358. <div class="section-label">渐变底部</div>
  1359. <div class="slider-item">
  1360. <input type="range" v-model.number="foamMatParams.gradientBottom" min="0" max="1" step="0.01" />
  1361. <span class="slider-value">{{ foamMatParams.gradientBottom.toFixed(2) }}</span>
  1362. </div>
  1363. </div>
  1364. <div class="material-section">
  1365. <div class="section-label">渐变曲线</div>
  1366. <div class="slider-item">
  1367. <input type="range" v-model.number="foamMatParams.gradientPower" min="0.1" max="5" step="0.1" />
  1368. <span class="slider-value">{{ foamMatParams.gradientPower.toFixed(1) }}</span>
  1369. </div>
  1370. </div>
  1371. <div class="material-section">
  1372. <button class="default-btn" @click="saveFoamDefaults">设置默认值</button>
  1373. </div>
  1374. </template>
  1375. <template v-if="selectedMaterialKey === 'flow'">
  1376. <div class="material-section">
  1377. <div class="section-label">颜色</div>
  1378. <div class="color-item">
  1379. <input type="color" v-model="flowParams.colour" />
  1380. <span class="color-value">{{ flowParams.colour }}</span>
  1381. </div>
  1382. </div>
  1383. <div class="material-section">
  1384. <div class="section-label">流动速度 X</div>
  1385. <div class="slider-item">
  1386. <input type="range" v-model.number="flowParams.speedX" min="-2" max="2" step="0.001" />
  1387. <span class="slider-value">{{ flowParams.speedX.toFixed(3) }}</span>
  1388. </div>
  1389. </div>
  1390. <div class="material-section">
  1391. <div class="section-label">流动速度 Y</div>
  1392. <div class="slider-item">
  1393. <input type="range" v-model.number="flowParams.speedY" min="-2" max="2" step="0.001" />
  1394. <span class="slider-value">{{ flowParams.speedY.toFixed(3) }}</span>
  1395. </div>
  1396. </div>
  1397. <div class="material-section">
  1398. <div class="section-label">平铺 U</div>
  1399. <div class="slider-item">
  1400. <input type="range" v-model.number="flowParams.tilingU" min="-5" max="5" step="0.01" />
  1401. <span class="slider-value">{{ flowParams.tilingU.toFixed(2) }}</span>
  1402. </div>
  1403. </div>
  1404. <div class="material-section">
  1405. <div class="section-label">平铺 V</div>
  1406. <div class="slider-item">
  1407. <input type="range" v-model.number="flowParams.tilingV" min="-5" max="5" step="0.01" />
  1408. <span class="slider-value">{{ flowParams.tilingV.toFixed(2) }}</span>
  1409. </div>
  1410. </div>
  1411. <div class="material-section">
  1412. <div class="section-label">旋转角度</div>
  1413. <div class="slider-item">
  1414. <input type="range" v-model.number="flowParams.rotationAngle" min="-1" max="1" step="0.01" />
  1415. <span class="slider-value">{{ flowParams.rotationAngle.toFixed(2) }}</span>
  1416. </div>
  1417. </div>
  1418. <div class="material-section">
  1419. <div class="section-label">泡沫强度</div>
  1420. <div class="slider-item">
  1421. <input type="range" v-model.number="flowParams.power" min="0" max="5" step="0.01" />
  1422. <span class="slider-value">{{ flowParams.power.toFixed(2) }}</span>
  1423. </div>
  1424. </div>
  1425. <div class="material-section">
  1426. <div class="section-label">水闸强度</div>
  1427. <div class="slider-item">
  1428. <input type="range" v-model.number="flowParams.waterGate" min="0" max="200" step="0.5" />
  1429. <span class="slider-value">{{ flowParams.waterGate.toFixed(1) }}</span>
  1430. </div>
  1431. </div>
  1432. <div class="material-section">
  1433. <div class="section-label">水闸翻转</div>
  1434. <div class="slider-item">
  1435. <input type="range" v-model.number="flowParams.waterGate02" min="0" max="1" step="0.01" />
  1436. <span class="slider-value">{{ flowParams.waterGate02.toFixed(2) }}</span>
  1437. </div>
  1438. </div>
  1439. <div class="material-section">
  1440. <div class="section-label">边缘遮罩强度</div>
  1441. <div class="slider-item">
  1442. <input type="range" v-model.number="flowParams.edgeMaskIntensity" min="0" max="1" step="0.01" />
  1443. <span class="slider-value">{{ flowParams.edgeMaskIntensity.toFixed(2) }}</span>
  1444. </div>
  1445. </div>
  1446. <div class="material-section">
  1447. <div class="section-label">不透明度功率</div>
  1448. <div class="slider-item">
  1449. <input type="range" v-model.number="flowParams.opaquePower" min="0" max="20" step="0.01" />
  1450. <span class="slider-value">{{ flowParams.opaquePower.toFixed(2) }}</span>
  1451. </div>
  1452. </div>
  1453. <div class="material-section">
  1454. <div class="section-label">边缘渐变淡出</div>
  1455. <div class="slider-item">
  1456. <input type="range" v-model.number="flowParams.edgeFade" min="0" max="0.5" step="0.01" />
  1457. <span class="slider-value">{{ flowParams.edgeFade.toFixed(2) }}</span>
  1458. </div>
  1459. </div>
  1460. <div class="material-section">
  1461. <button class="default-btn" @click="saveFlowDefaults">设置默认值</button>
  1462. </div>
  1463. </template>
  1464. <template v-if="selectedMaterialKey === 'cscwater'">
  1465. <div class="material-section">
  1466. <div class="section-label">透明度</div>
  1467. <div class="slider-item">
  1468. <input type="range" v-model.number="cscwaterParams.alpha" min="0" max="1" step="0.01" />
  1469. <span class="slider-value">{{ cscwaterParams.alpha.toFixed(2) }}</span>
  1470. </div>
  1471. </div>
  1472. <div class="material-section">
  1473. <div class="section-label">浪高</div>
  1474. <div class="slider-item">
  1475. <input type="range" v-model.number="cscwaterParams.waveHeight" min="0" max="2" step="0.05" />
  1476. <span class="slider-value">{{ cscwaterParams.waveHeight.toFixed(2) }}</span>
  1477. </div>
  1478. </div>
  1479. <div class="material-section">
  1480. <div class="section-label">水流速度</div>
  1481. <div class="slider-item">
  1482. <input type="range" v-model.number="cscwaterParams.flowSpeed" min="0" max="3" step="0.1" />
  1483. <span class="slider-value">{{ cscwaterParams.flowSpeed.toFixed(1) }}</span>
  1484. </div>
  1485. </div>
  1486. <div class="material-section">
  1487. <div class="section-label">流向 X</div>
  1488. <div class="slider-item">
  1489. <input type="range" v-model.number="cscwaterParams.flowDirectionX" min="-2" max="2" step="0.1" />
  1490. <span class="slider-value">{{ cscwaterParams.flowDirectionX.toFixed(1) }}</span>
  1491. </div>
  1492. </div>
  1493. <div class="material-section">
  1494. <div class="section-label">流向 Z</div>
  1495. <div class="slider-item">
  1496. <input type="range" v-model.number="cscwaterParams.flowDirectionY" min="-2" max="2" step="0.1" />
  1497. <span class="slider-value">{{ cscwaterParams.flowDirectionY.toFixed(1) }}</span>
  1498. </div>
  1499. </div>
  1500. <div class="material-section">
  1501. <div class="section-label">法线旋转</div>
  1502. <div class="slider-item">
  1503. <input type="range" v-model.number="cscwaterParams.normalRotation" min="-180" max="180" step="1" />
  1504. <span class="slider-value">{{ cscwaterParams.normalRotation.toFixed(0) }}°</span>
  1505. </div>
  1506. </div>
  1507. <div class="material-section">
  1508. <div class="section-label">浅水颜色</div>
  1509. <div class="color-item">
  1510. <input type="color" v-model="cscwaterParams.waterColor" />
  1511. <span class="color-value">{{ cscwaterParams.waterColor }}</span>
  1512. </div>
  1513. </div>
  1514. <div class="material-section">
  1515. <div class="section-label">深水颜色</div>
  1516. <div class="color-item">
  1517. <input type="color" v-model="cscwaterParams.deepColor" />
  1518. <span class="color-value">{{ cscwaterParams.deepColor }}</span>
  1519. </div>
  1520. </div>
  1521. <div class="material-section">
  1522. <div class="section-label">水深范围</div>
  1523. <div class="slider-item">
  1524. <input type="range" v-model.number="cscwaterParams.depthRange" min="1" max="50" step="0.5" />
  1525. <span class="slider-value">{{ cscwaterParams.depthRange.toFixed(1) }}</span>
  1526. </div>
  1527. </div>
  1528. <div class="material-section">
  1529. <div class="section-label">水法线强度</div>
  1530. <div class="slider-item">
  1531. <input type="range" v-model.number="cscwaterParams.waterNormalStrength" min="0" max="2" step="0.05" />
  1532. <span class="slider-value">{{ cscwaterParams.waterNormalStrength.toFixed(2) }}</span>
  1533. </div>
  1534. </div>
  1535. <div class="material-section">
  1536. <div class="section-label">水纹平铺</div>
  1537. <div class="slider-item">
  1538. <input type="range" v-model.number="cscwaterParams.waterNormalTiling" min="0.01" max="5" step="0.01" />
  1539. <span class="slider-value">{{ cscwaterParams.waterNormalTiling.toFixed(2) }}</span>
  1540. </div>
  1541. </div>
  1542. <div class="material-section">
  1543. <div class="section-label">高光强度</div>
  1544. <div class="slider-item">
  1545. <input type="range" v-model.number="cscwaterParams.specIntensity" min="0" max="5" step="0.1" />
  1546. <span class="slider-value">{{ cscwaterParams.specIntensity.toFixed(1) }}</span>
  1547. </div>
  1548. </div>
  1549. <div class="material-section">
  1550. <div class="section-label">高光锐度</div>
  1551. <div class="slider-item">
  1552. <input type="range" v-model.number="cscwaterParams.specPower" min="1" max="256" step="1" />
  1553. <span class="slider-value">{{ cscwaterParams.specPower.toFixed(0) }}</span>
  1554. </div>
  1555. </div>
  1556. <div class="material-section">
  1557. <div class="section-label">菲涅尔功率</div>
  1558. <div class="slider-item">
  1559. <input type="range" v-model.number="cscwaterParams.fresnelPower" min="0.1" max="10" step="0.1" />
  1560. <span class="slider-value">{{ cscwaterParams.fresnelPower.toFixed(1) }}</span>
  1561. </div>
  1562. </div>
  1563. <div class="material-section">
  1564. <div class="section-label">菲涅尔强度</div>
  1565. <div class="slider-item">
  1566. <input type="range" v-model.number="cscwaterParams.fresnelIntensity" min="0" max="3" step="0.1" />
  1567. <span class="slider-value">{{ cscwaterParams.fresnelIntensity.toFixed(1) }}</span>
  1568. </div>
  1569. </div>
  1570. <div class="material-section">
  1571. <div class="section-label">泡沫强度</div>
  1572. <div class="slider-item">
  1573. <input type="range" v-model.number="cscwaterParams.foamIntensity" min="0" max="2" step="0.05" />
  1574. <span class="slider-value">{{ cscwaterParams.foamIntensity.toFixed(2) }}</span>
  1575. </div>
  1576. </div>
  1577. <div class="material-section">
  1578. <div class="section-label">碰撞泡沫阈值</div>
  1579. <div class="slider-item">
  1580. <input type="range" v-model.number="cscwaterParams.collisionFoamThreshold" min="0" max="1" step="0.01" />
  1581. <span class="slider-value">{{ cscwaterParams.collisionFoamThreshold.toFixed(2) }}</span>
  1582. </div>
  1583. </div>
  1584. <div class="material-section">
  1585. <div class="section-label">碰撞泡沫强度</div>
  1586. <div class="slider-item">
  1587. <input type="range" v-model.number="cscwaterParams.collisionFoamStrength" min="0" max="2" step="0.05" />
  1588. <span class="slider-value">{{ cscwaterParams.collisionFoamStrength.toFixed(2) }}</span>
  1589. </div>
  1590. </div>
  1591. <div class="material-section">
  1592. <button class="default-btn" @click="saveCscwaterDefaults">设置默认值</button>
  1593. </div>
  1594. </template>
  1595. </div>
  1596. <!-- 视角选择面板 -->
  1597. <div v-if="props.showDebugTools && showCameraPresetPanel" class="panel camera-preset-panel">
  1598. <div class="panel-header">
  1599. <span class="panel-title">视角选择</span>
  1600. <button class="toggle-btn" @click="showCameraPresetPanel = false">×</button>
  1601. </div>
  1602. <div class="preset-list">
  1603. <div
  1604. v-for="preset in cameraPresets"
  1605. :key="preset.id"
  1606. class="preset-item"
  1607. @click="showCameraPresetPanel = false; flyToPreset(preset.id)"
  1608. >
  1609. <span class="preset-name">{{ preset.name }}</span>
  1610. </div>
  1611. </div>
  1612. </div>
  1613. </template>
  1614. <style scoped>
  1615. /* 全屏 3D 渲染容器 */
  1616. .scene-container {
  1617. position: fixed;
  1618. top: 0;
  1619. left: 0;
  1620. width: 100vw;
  1621. height: 100vh;
  1622. overflow: hidden;
  1623. }
  1624. /* 右上角调试工具栏 */
  1625. .toolbar {
  1626. position: fixed;
  1627. top: 20px;
  1628. right: 20px;
  1629. display: flex;
  1630. flex-direction: column;
  1631. gap: 8px;
  1632. z-index: 1001;
  1633. }
  1634. .toolbar-btn {
  1635. padding: 10px 16px;
  1636. border: none;
  1637. border-radius: 8px;
  1638. background: rgba(0, 0, 0, 0.85);
  1639. color: #888;
  1640. font-size: 13px;
  1641. font-weight: bold;
  1642. cursor: pointer;
  1643. transition: all 0.2s;
  1644. min-width: 70px;
  1645. }
  1646. .toolbar-btn:hover {
  1647. background: rgba(30, 30, 30, 0.9);
  1648. color: #fff;
  1649. }
  1650. .toolbar-btn.active {
  1651. color: #fff;
  1652. box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
  1653. }
  1654. .toolbar-btn:nth-child(1).active { color: #4fc3f7; }
  1655. .toolbar-btn:nth-child(2).active { color: #ff9800; }
  1656. .toolbar-btn:nth-child(3).active { color: #ff4081; }
  1657. .toolbar-btn:nth-child(4).active { color: #4dd0e1; }
  1658. .toolbar-btn:nth-child(5).active { color: #ffeb3b; }
  1659. /* 调试面板通用样式 */
  1660. .panel {
  1661. position: fixed;
  1662. top: 20px;
  1663. left: 20px;
  1664. background: rgba(0, 0, 0, 0.85);
  1665. color: white;
  1666. padding: 15px;
  1667. border-radius: 8px;
  1668. font-family: 'Courier New', monospace;
  1669. z-index: 1000;
  1670. max-height: calc(100vh - 40px);
  1671. overflow-y: auto;
  1672. }
  1673. .coordinate-panel { min-width: 200px; }
  1674. .camera-panel { min-width: 200px; }
  1675. .model-panel { min-width: 260px; }
  1676. .material-panel { min-width: 260px; }
  1677. .camera-preset-panel {
  1678. min-width: 200px;
  1679. left: auto;
  1680. right: 20px;
  1681. top: auto;
  1682. bottom: 20px;
  1683. }
  1684. .preset-list {
  1685. display: flex;
  1686. flex-direction: column;
  1687. gap: 4px;
  1688. margin-top: 8px;
  1689. }
  1690. .preset-item {
  1691. padding: 8px 12px;
  1692. border-radius: 6px;
  1693. background: rgba(255, 255, 255, 0.08);
  1694. color: #ccc;
  1695. font-size: 13px;
  1696. cursor: pointer;
  1697. transition: all 0.2s;
  1698. }
  1699. .preset-item:hover {
  1700. background: rgba(255, 235, 59, 0.2);
  1701. color: #fff;
  1702. }
  1703. .preset-name {
  1704. font-family: 'Microsoft YaHei', sans-serif;
  1705. }
  1706. .panel-header {
  1707. display: flex;
  1708. justify-content: space-between;
  1709. align-items: center;
  1710. margin-bottom: 12px;
  1711. padding-bottom: 8px;
  1712. border-bottom: 1px solid rgba(255, 255, 255, 0.3);
  1713. }
  1714. .panel-title {
  1715. font-weight: bold;
  1716. font-size: 14px;
  1717. }
  1718. .coordinate-panel .panel-title { color: #4fc3f7; }
  1719. .camera-panel .panel-title { color: #ff9800; }
  1720. .model-panel .panel-title { color: #ff4081; }
  1721. .material-panel .panel-title { color: #4dd0e1; }
  1722. .camera-preset-panel .panel-title { color: #ffeb3b; }
  1723. /* 关闭按钮 */
  1724. .toggle-btn {
  1725. width: 22px;
  1726. height: 22px;
  1727. border: none;
  1728. border-radius: 4px;
  1729. background: rgba(255, 255, 255, 0.15);
  1730. color: white;
  1731. font-size: 14px;
  1732. line-height: 1;
  1733. cursor: pointer;
  1734. display: flex;
  1735. align-items: center;
  1736. justify-content: center;
  1737. transition: background 0.2s;
  1738. }
  1739. .toggle-btn:hover {
  1740. background: rgba(255, 255, 255, 0.3);
  1741. }
  1742. /* 坐标列表 */
  1743. .coordinate-item {
  1744. display: flex;
  1745. justify-content: space-between;
  1746. margin: 5px 0;
  1747. }
  1748. .coordinate-label {
  1749. color: #81c784;
  1750. font-weight: bold;
  1751. }
  1752. .coordinate-value {
  1753. color: #fff;
  1754. }
  1755. /* 相机信息分组 */
  1756. .camera-section {
  1757. margin-bottom: 10px;
  1758. }
  1759. .section-label {
  1760. color: #81c784;
  1761. font-size: 11px;
  1762. margin-bottom: 5px;
  1763. font-weight: bold;
  1764. }
  1765. .camera-item {
  1766. display: flex;
  1767. justify-content: space-between;
  1768. margin: 3px 0;
  1769. }
  1770. .camera-label {
  1771. color: #4fc3f7;
  1772. }
  1773. .camera-value {
  1774. color: #fff;
  1775. }
  1776. /* 模型/材质分组 */
  1777. .model-section,
  1778. .material-section {
  1779. margin-bottom: 12px;
  1780. }
  1781. .model-section .section-label,
  1782. .material-section .section-label {
  1783. color: #81c784;
  1784. font-size: 11px;
  1785. margin-bottom: 6px;
  1786. font-weight: bold;
  1787. }
  1788. /* 下拉选择器 */
  1789. .model-select {
  1790. width: 100%;
  1791. padding: 6px 8px;
  1792. border: 1px solid rgba(255, 255, 255, 0.2);
  1793. border-radius: 6px;
  1794. background: rgba(255, 255, 255, 0.08);
  1795. color: #fff;
  1796. font-size: 13px;
  1797. font-family: inherit;
  1798. cursor: pointer;
  1799. outline: none;
  1800. }
  1801. .model-select option {
  1802. background: #222;
  1803. color: #fff;
  1804. }
  1805. /* 滑条控件 */
  1806. .slider-item {
  1807. display: flex;
  1808. align-items: center;
  1809. gap: 10px;
  1810. margin-bottom: 4px;
  1811. }
  1812. .slider-label {
  1813. font-size: 12px;
  1814. color: #aaa;
  1815. min-width: 32px;
  1816. }
  1817. .slider-item input[type="range"] {
  1818. flex: 1;
  1819. height: 4px;
  1820. -webkit-appearance: none;
  1821. appearance: none;
  1822. background: rgba(255, 255, 255, 0.15);
  1823. border-radius: 2px;
  1824. outline: none;
  1825. }
  1826. .slider-item input[type="range"]::-webkit-slider-thumb {
  1827. -webkit-appearance: none;
  1828. appearance: none;
  1829. width: 12px;
  1830. height: 12px;
  1831. border-radius: 50%;
  1832. background: #ff4081;
  1833. cursor: pointer;
  1834. }
  1835. .slider-value {
  1836. min-width: 50px;
  1837. text-align: right;
  1838. color: #fff;
  1839. font-size: 11px;
  1840. font-family: 'Consolas', monospace;
  1841. }
  1842. /* 数字输入框 */
  1843. .input-item {
  1844. display: flex;
  1845. align-items: center;
  1846. gap: 10px;
  1847. margin-bottom: 4px;
  1848. }
  1849. .input-label {
  1850. font-size: 12px;
  1851. color: #aaa;
  1852. min-width: 32px;
  1853. }
  1854. .number-input {
  1855. flex: 1;
  1856. padding: 4px 8px;
  1857. border: 1px solid rgba(255, 255, 255, 0.2);
  1858. border-radius: 6px;
  1859. background: rgba(255, 255, 255, 0.08);
  1860. color: #fff;
  1861. font-size: 13px;
  1862. font-family: 'Consolas', monospace;
  1863. outline: none;
  1864. }
  1865. .number-input:focus {
  1866. border-color: #ff4081;
  1867. }
  1868. /* 颜色拾取器 */
  1869. .color-item {
  1870. display: flex;
  1871. align-items: center;
  1872. gap: 10px;
  1873. }
  1874. .color-item input[type="color"] {
  1875. width: 40px;
  1876. height: 28px;
  1877. border: none;
  1878. border-radius: 4px;
  1879. cursor: pointer;
  1880. background: transparent;
  1881. }
  1882. .color-item input[type="color"]::-webkit-color-swatch-wrapper {
  1883. padding: 0;
  1884. }
  1885. .color-item input[type="color"]::-webkit-color-swatch {
  1886. border: none;
  1887. border-radius: 4px;
  1888. }
  1889. .color-value {
  1890. color: #fff;
  1891. font-size: 11px;
  1892. font-family: 'Consolas', monospace;
  1893. }
  1894. /* 材质面板滑条颜色(区别于模型的粉色,材质用青色) */
  1895. .material-section .slider-item input[type="range"] {
  1896. background: rgba(255, 255, 255, 0.15);
  1897. }
  1898. .material-section .slider-item input[type="range"]::-webkit-slider-thumb {
  1899. background: #4dd0e1;
  1900. }
  1901. /* 保存默认值按钮 */
  1902. .default-btn {
  1903. width: 100%;
  1904. padding: 8px 16px;
  1905. border: 1px solid rgba(255, 255, 255, 0.2);
  1906. border-radius: 6px;
  1907. background: rgba(255, 255, 255, 0.08);
  1908. color: #4dd0e1;
  1909. font-size: 13px;
  1910. cursor: pointer;
  1911. transition: all 0.2s;
  1912. }
  1913. .default-btn:hover {
  1914. background: rgba(77, 208, 225, 0.15);
  1915. border-color: #4dd0e1;
  1916. }
  1917. /* 底部提示消息 */
  1918. .toast-message {
  1919. position: fixed;
  1920. bottom: 40px;
  1921. left: 50%;
  1922. transform: translateX(-50%);
  1923. background: rgba(0, 0, 0, 0.85);
  1924. color: #4dd0e1;
  1925. padding: 10px 24px;
  1926. border-radius: 8px;
  1927. font-size: 14px;
  1928. font-family: 'Microsoft YaHei', sans-serif;
  1929. z-index: 9999;
  1930. pointer-events: none;
  1931. transition: opacity 0.3s ease;
  1932. border: 1px solid rgba(77, 208, 225, 0.3);
  1933. }
  1934. .toast-message.toast-fade {
  1935. opacity: 0;
  1936. }
  1937. </style>