| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115 |
- <script setup lang="ts">
- import { onMounted, onUnmounted, ref, reactive, watch } from 'vue'
- import * as THREE from 'three'
- import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
- import { Sky } from 'three/examples/jsm/objects/Sky.js'
- import { StylizedWaterMaterial } from '../materials/waterNew'
- import { createWaterFoamUEMaterial } from '../materials/waterFoamUE'
- import { createWaterFlowMaterial } from '../materials/waterFlow'
- import foamTexUrl from '../assets/texture/T_Waterfall_Foam.PNG'
- import directionalFoamTexUrl from '../assets/texture/T_Waterfall_Foam_Directional.PNG'
- import flowTexUrl from '../assets/texture/FlowTexture/T_FlowTexture_BC.PNG'
- import foamMacroTexUrl from '../assets/texture/FlowTexture/T_FoamMacro_BC.PNG'
- import maskTexUrl from '../assets/texture/FlowTexture/MASK_003.PNG'
- import flowNormalTexUrl from '../assets/texture/FlowTexture/T_FlowTexture_BC_NORM.PNG'
- import foamMacroNormalTexUrl from '../assets/texture/FlowTexture/T_FoamMacro_BC_NORM.PNG'
- import langGLBUrl from '../assets/lang.glb'
- import cscwaterGLBUrl from '../assets/CSCwater.glb'
- import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
- import { TilesRenderer } from '3d-tiles-renderer'
- import { ReorientationPlugin } from '3d-tiles-renderer/plugins'
- import WaterLevelLabel from './WaterLevelLabel.vue'
- import {
- modelTransformMap as configModelTransformMap,
- defaultWaterParams,
- defaultCscwaterParams,
- defaultFoamMatParams,
- defaultFlowParams,
- type ModelTransform,
- type WaterMaterialParams,
- type CscwaterMaterialParams,
- type FoamMaterialParams,
- type FlowMaterialParams,
- sceneLabels,
- type WaterLevelLabelConfig,
- cameraPresets,
- } from '../config/sceneConfig'
- // ==================== 外部 Props(嵌入其他项目时使用)====================
- const props = withDefaults(defineProps<{
- labelValues?: Record<string, number>
- showDebugTools?: boolean
- tilesetUrl?: string
- cameraPosition?: { x: number; y: number; z: number }
- cameraTarget?: { x: number; y: number; z: number }
- }>(), {
- labelValues: () => ({}),
- showDebugTools: false,
- tilesetUrl: '/scene/tileset.json',
- cameraPosition: () => ({ x: -831.56685, y: 40.63456, z: -2225.321 }),
- cameraTarget: () => ({ x: -843.0744, y: 12.01539, z: -2182.06814 }),
- })
- // 继承 3D Tiles 渲染器,修复 asset 字段缺失的兼容性问题
- class SuperMapTilesRenderer extends TilesRenderer {
- preprocessTileset(json: any, url: string, parent = null) {
- if (!json.asset) {
- json.asset = { version: '1.0' }
- }
- const parentProto = Object.getPrototypeOf(Object.getPrototypeOf(this))
- parentProto.preprocessTileset.call(this, json, url, parent)
- }
- }
- // 3D 场景挂载点的 DOM 引用
- const containerRef = ref<HTMLDivElement>()
- // 底部提示消息弹窗
- function showToast(msg: string) {
- const el = document.createElement('div')
- el.className = 'toast-message'
- el.textContent = msg
- document.body.appendChild(el)
- setTimeout(() => {
- el.classList.add('toast-fade')
- setTimeout(() => el.remove(), 300)
- }, 1500)
- }
- // 鼠标拾取到的坐标
- const pickedPosition = ref<{ x: number; y: number; z: number } | null>({ x: 0, y: 0, z: 0 })
- // 相机位置/目标/距离等信息
- const cameraInfo = ref({
- position: { x: 0, y: 0, z: 0 },
- target: { x: 0, y: 0, z: 0 },
- distance: 0,
- minDistance: 0,
- maxDistance: 0,
- })
- // 调试面板的显示开关
- const showCoordinatePanel = ref(false)
- const showCameraPanel = ref(false)
- const showModelPanel = ref(false)
- const showMaterialPanel = ref(false)
- const showCameraPresetPanel = ref(false)
- // ========== 水面材质参数 ==========
- const waterParams = ref<WaterMaterialParams>({ ...defaultWaterParams })
- // ========== CSCwater 独立材质参数(与主水面分开调节)==========
- const cscwaterParams = ref<CscwaterMaterialParams>({ ...defaultCscwaterParams })
- // ========== 泡沫片面材质参数 ==========
- const foamMatParams = ref<FoamMaterialParams>({ ...defaultFoamMatParams })
- // ========== 流动纹理材质参数 ==========
- const flowParams = ref<FlowMaterialParams>({ ...defaultFlowParams })
- // ---- 从 localStorage 恢复/保存材质默认值 ----
- const FLOW_DEFAULTS_KEY = 'flowMatParams_defaults'
- function loadFlowDefaults() {
- const saved = localStorage.getItem(FLOW_DEFAULTS_KEY)
- if (saved) {
- try {
- const parsed = JSON.parse(saved)
- Object.assign(flowParams.value, parsed)
- } catch (e) {
- console.warn('Failed to parse flow defaults:', e)
- }
- }
- }
- function saveFlowDefaults() {
- localStorage.setItem(FLOW_DEFAULTS_KEY, JSON.stringify(flowParams.value))
- showToast('流动纹理材质默认值已保存')
- }
- // 将 flow 材质参数同步到着色器的 uniform 变量
- function syncFlowParams() {
- if (!flowMaterial) return
- const p = flowParams.value
- flowMaterial.uniforms.uColor.value.set(p.colour)
- flowMaterial.uniforms.uSpeedX.value = p.speedX
- flowMaterial.uniforms.uSpeedY.value = p.speedY
- flowMaterial.uniforms.uTilingU.value = p.tilingU
- flowMaterial.uniforms.uTilingV.value = p.tilingV
- flowMaterial.uniforms.uRotationAngle.value = p.rotationAngle
- flowMaterial.uniforms.uPower.value = p.power
- flowMaterial.uniforms.uWaterGate.value = p.waterGate
- flowMaterial.uniforms.uWaterGate02.value = p.waterGate02
- flowMaterial.uniforms.uEdgeMaskIntensity.value = p.edgeMaskIntensity
- flowMaterial.uniforms.uOpaquePower.value = p.opaquePower
- flowMaterial.uniforms.uEdgeFade.value = p.edgeFade
- }
- // 将 cscwater 材质参数同步到着色器的 uniform 变量
- function syncCscwaterParams() {
- if (!cscwaterMaterial) return
- const p = cscwaterParams.value
- cscwaterMaterial.uniforms.alpha.value = p.alpha
- cscwaterMaterial.uniforms.flowSpeed.value = p.flowSpeed
- cscwaterMaterial.uniforms.flowDirection.value.set(p.flowDirectionX, p.flowDirectionY)
- cscwaterMaterial.uniforms.normalRotation.value = p.normalRotation
- cscwaterMaterial.uniforms.waveHeight.value = p.waveHeight
- cscwaterMaterial.uniforms.shallowColor.value.set(p.waterColor)
- cscwaterMaterial.uniforms.deepColor.value.set(p.deepColor)
- cscwaterMaterial.uniforms.foamIntensity.value = p.foamIntensity
- cscwaterMaterial.uniforms.specIntensity.value = p.specIntensity
- cscwaterMaterial.uniforms.specPower.value = p.specPower
- cscwaterMaterial.uniforms.fresnelPower.value = p.fresnelPower
- cscwaterMaterial.uniforms.fresnelIntensity.value = p.fresnelIntensity
- cscwaterMaterial.uniforms.depthRange.value = p.depthRange
- cscwaterMaterial.uniforms.waterNormalStrength.value = p.waterNormalStrength
- cscwaterMaterial.uniforms.waterNormalTiling.value = p.waterNormalTiling
- cscwaterMaterial.uniforms.collisionFoamThreshold.value = p.collisionFoamThreshold
- cscwaterMaterial.uniforms.collisionFoamStrength.value = p.collisionFoamStrength
- }
- const FOAM_DEFAULTS_KEY = 'foamMatParams_defaults'
- function loadFoamDefaults() {
- const saved = localStorage.getItem(FOAM_DEFAULTS_KEY)
- if (saved) {
- try {
- const parsed = JSON.parse(saved)
- Object.assign(foamMatParams.value, parsed)
- } catch (e) {
- console.warn('Failed to parse foam defaults:', e)
- }
- }
- }
- function saveFoamDefaults() {
- localStorage.setItem(FOAM_DEFAULTS_KEY, JSON.stringify(foamMatParams.value))
- showToast('泡沫材质默认值已保存')
- }
- const WATER_DEFAULTS_KEY = 'waterParams_defaults'
- function loadWaterDefaults() {
- const saved = localStorage.getItem(WATER_DEFAULTS_KEY)
- if (saved) {
- try {
- const parsed = JSON.parse(saved)
- Object.assign(waterParams.value, parsed)
- } catch (e) {
- console.warn('Failed to parse water defaults:', e)
- }
- }
- }
- function saveWaterDefaults() {
- localStorage.setItem(WATER_DEFAULTS_KEY, JSON.stringify(waterParams.value))
- showToast('水面材质默认值已保存')
- }
- const CSCWATER_DEFAULTS_KEY = 'cscwaterParams_defaults'
- function loadCscwaterDefaults() {
- const saved = localStorage.getItem(CSCWATER_DEFAULTS_KEY)
- if (saved) {
- try {
- const parsed = JSON.parse(saved)
- Object.assign(cscwaterParams.value, parsed)
- } catch (e) {
- console.warn('Failed to parse cscwater defaults:', e)
- }
- }
- }
- function saveCscwaterDefaults() {
- localStorage.setItem(CSCWATER_DEFAULTS_KEY, JSON.stringify(cscwaterParams.value))
- showToast('CSCwater 材质默认值已保存')
- }
- const MODEL_DEFAULTS_KEY = 'modelTransform_defaults'
- function saveModelDefaults() {
- const t = modelTransform.value
- modelTransformMap[selectedModelKey.value] = {
- positionX: t.positionX,
- positionY: t.positionY,
- positionZ: t.positionZ,
- rotationX: t.rotationX,
- rotationY: t.rotationY,
- rotationZ: t.rotationZ,
- scaleX: t.scaleX,
- scaleY: t.scaleY,
- scaleZ: t.scaleZ,
- }
- const data: Record<string, typeof modelTransformMap.foam> = {}
- for (const key in modelTransformMap) {
- data[key] = { ...modelTransformMap[key] }
- }
- localStorage.setItem(MODEL_DEFAULTS_KEY, JSON.stringify(data))
- showToast('模型变换默认值已保存')
- }
- // 场景中可调试的模型列表,按 key 索引
- const modelList: Record<string, THREE.Object3D | null> = {}
- const selectedModelKey = ref('foam')
- const materialList: Record<string, THREE.Material | null> = {}
- const selectedMaterialKey = ref('water')
- // 各模型的初始变换参数(位置/旋转/缩放)
- const modelTransformMap: Record<string, ModelTransform> = {}
- for (const key of Object.keys(configModelTransformMap)) {
- modelTransformMap[key] = { ...configModelTransformMap[key] }
- }
- // 当前选中的模型变换值(双向绑定)
- const modelTransform = ref({ ...modelTransformMap.foam })
- // 将 modelTransform 的数值应用到 Three.js 物体上
- function applyModelTransform(key: string) {
- const obj = modelList[key]
- if (!obj) return
- const t = modelTransform.value
- if (key === 'foam' && 'isMesh' in obj) {
- const mesh = obj as THREE.Mesh
- mesh.position.set(t.positionX, t.positionY, t.positionZ)
- mesh.rotation.order = 'YXZ'
- mesh.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- if (mesh.geometry.type === 'PlaneGeometry') {
- const geom = mesh.geometry as THREE.PlaneGeometry
- geom.dispose()
- mesh.geometry = new THREE.PlaneGeometry(t.scaleX, t.scaleY)
- }
- } else if (key === 'water') {
- obj.position.set(t.positionX, t.positionY, t.positionZ)
- obj.rotation.order = 'YXZ'
- obj.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- } else if (key === 'water2') {
- obj.position.set(t.positionX, t.positionY, t.positionZ)
- obj.rotation.order = 'YXZ'
- obj.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- } else if (key === 'flow') {
- obj.position.set(t.positionX, t.positionY, t.positionZ)
- obj.rotation.order = 'YXZ'
- obj.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- obj.scale.set(t.scaleX, t.scaleY, t.scaleZ)
- } else if (key === 'cscwater') {
- obj.position.set(t.positionX, t.positionY, t.positionZ)
- obj.rotation.order = 'YXZ'
- obj.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- obj.scale.set(t.scaleX, t.scaleY, t.scaleZ)
- }
- }
- // 切换模型时,加载该模型保存的变换参数
- watch(selectedModelKey, (key) => {
- const saved = modelTransformMap[key]
- if (saved) {
- modelTransform.value = { ...saved }
- }
- })
- // 变换参数变化时实时应用到场景
- watch(modelTransform, () => {
- applyModelTransform(selectedModelKey.value)
- }, { deep: true })
- // 水位标签
- interface LabelData {
- config: WaterLevelLabelConfig
- componentRef: ReturnType<typeof ref<InstanceType<typeof WaterLevelLabel> | null>>
- }
- const labelDataList: LabelData[] = []
- const labelValues = reactive<Record<string, number>>({})
- function initLabelData() {
- labelDataList.length = 0
- for (const cfg of sceneLabels.main) {
- const externalValue = props.labelValues[cfg.id]
- labelValues[cfg.id] = externalValue !== undefined ? externalValue : cfg.initialValue
- labelDataList.push({
- config: cfg,
- componentRef: ref<InstanceType<typeof WaterLevelLabel> | null>(null),
- })
- }
- }
- initLabelData()
- // ========== Three.js 核心变量声明 ==========
- let scene: THREE.Scene
- let camera: THREE.PerspectiveCamera
- let renderer: THREE.WebGLRenderer
- let controls: OrbitControls
- let sky: Sky
- let waterMesh: THREE.Mesh
- let water2Mesh: THREE.Mesh | null = null
- let foamMesh: THREE.Mesh | null = null
- let foamMaterial: THREE.ShaderMaterial | null = null
- let flowMesh: THREE.Mesh | null = null
- let flowMaterial: THREE.ShaderMaterial | null = null
- let cscwaterModel: THREE.Object3D | null = null
- let cscwaterMaterial: THREE.ShaderMaterial | null = null
- let sunDirection: THREE.Vector3
- let animationId: number
- let tilesRenderer: SuperMapTilesRenderer | null = null
- let raycaster: THREE.Raycaster
- let mouse: THREE.Vector2
- let depthRenderTarget: THREE.WebGLRenderTarget
- let sceneInitialized = false
- // 创建天空背景(含体积云效果)
- function createSky() {
- sky = new Sky()
- sky.scale.setScalar(50000)
- scene.add(sky)
- const uniforms = sky.material.uniforms
- uniforms.turbidity.value = 6
- uniforms.rayleigh.value = 0.1
- uniforms.mieCoefficient.value = 0.005
- uniforms.mieDirectionalG.value = 0.7
- uniforms.sunPosition.value.set(20, 30, 10)
- uniforms.cloudScale.value = 0.0008
- uniforms.cloudSpeed.value = 0.00015
- uniforms.cloudCoverage.value = 0.6
- uniforms.cloudDensity.value = 0.6
- uniforms.cloudElevation.value = 0.9
- }
- // 创建水面网格(使用自定义风格化水材质)
- function createWaterSurface() {
- const t = modelTransformMap.water
- waterMesh = new THREE.Mesh(
- new THREE.PlaneGeometry(t.scaleX, t.scaleY, 120, 120),
- StylizedWaterMaterial
- )
- waterMesh.rotation.order = 'YXZ'
- waterMesh.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- waterMesh.position.set(t.positionX, t.positionY, t.positionZ)
- waterMesh.receiveShadow = true
- waterMesh.name = 'water'
- waterMesh.renderOrder = 0
- scene.add(waterMesh)
- modelList['water'] = waterMesh
- materialList['water'] = StylizedWaterMaterial
- StylizedWaterMaterial.uniforms.cameraPos.value.copy(camera.position)
- StylizedWaterMaterial.uniforms.sunDirection.value.copy(sunDirection)
- StylizedWaterMaterial.uniforms.cameraNear.value = camera.near
- StylizedWaterMaterial.uniforms.cameraFar.value = camera.far
- const container = containerRef.value!
- StylizedWaterMaterial.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight)
- }
- // 创建第二片面(使用与主水面相同的风格化水材质)
- function createWater2Surface() {
- const t = modelTransformMap.water2
- water2Mesh = new THREE.Mesh(
- new THREE.PlaneGeometry(t.scaleX, t.scaleY, 120, 120),
- StylizedWaterMaterial
- )
- water2Mesh.rotation.order = 'YXZ'
- water2Mesh.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- water2Mesh.position.set(t.positionX, t.positionY, t.positionZ)
- water2Mesh.receiveShadow = true
- water2Mesh.name = 'water2'
- water2Mesh.renderOrder = 0
- scene.add(water2Mesh)
- modelList['water2'] = water2Mesh
- }
- // 创建泡沫片面(瀑布泡沫效果)
- function createWaterFoamSurface() {
- const textureLoader = new THREE.TextureLoader()
- const foamTexture = textureLoader.load(foamTexUrl)
- foamTexture.wrapS = THREE.RepeatWrapping
- foamTexture.wrapT = THREE.RepeatWrapping
- const directionalFoamTexture = textureLoader.load(directionalFoamTexUrl)
- directionalFoamTexture.wrapS = THREE.RepeatWrapping
- directionalFoamTexture.wrapT = THREE.RepeatWrapping
- foamMaterial = createWaterFoamUEMaterial({
- colour: new THREE.Color(foamMatParams.value.colour),
- opacity: foamMatParams.value.opacity,
- waterfallSpeed: foamMatParams.value.waterfallSpeed,
- edgeMaskTiling: foamMatParams.value.edgeMaskTiling,
- edgeMaskSpeed: foamMatParams.value.edgeMaskSpeed,
- fresnelExponent: foamMatParams.value.fresnelExponent,
- directionalFoamIntensity: foamMatParams.value.directionalFoamIntensity,
- directionalFoamContrast: foamMatParams.value.directionalFoamContrast,
- directionalFoam1Intensity: foamMatParams.value.directionalFoam1Intensity,
- directionalFoam2Intensity: foamMatParams.value.directionalFoam2Intensity,
- directionalFoam2Tiling: foamMatParams.value.directionalFoam2Tiling,
- directionalFoam2Speed: foamMatParams.value.directionalFoam2Speed,
- directionalFoam3Intensity: foamMatParams.value.directionalFoam3Intensity,
- foamFalloff: foamMatParams.value.foamFalloff,
- gradientTop: foamMatParams.value.gradientTop,
- gradientBottom: foamMatParams.value.gradientBottom,
- gradientPower: foamMatParams.value.gradientPower,
- foamTexture,
- directionalFoamTexture,
- })
- const t = modelTransformMap.foam
- const geometry = new THREE.PlaneGeometry(t.scaleX, t.scaleY)
- foamMesh = new THREE.Mesh(geometry, foamMaterial)
- foamMesh.rotation.order = 'YXZ'
- foamMesh.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- foamMesh.position.set(t.positionX, t.positionY, t.positionZ)
- foamMesh.name = 'foam'
- foamMesh.renderOrder = 1
- scene.add(foamMesh)
- modelList['foam'] = foamMesh
- materialList['foam'] = foamMaterial
- }
- // 加载流动水纹理模型(GLB 模型+流动纹理材质)
- function loadWaterFlowModel() {
- const textureLoader = new THREE.TextureLoader()
- const flowTexture = textureLoader.load(flowTexUrl)
- flowTexture.wrapS = THREE.RepeatWrapping
- flowTexture.wrapT = THREE.RepeatWrapping
- const foamMacroTexture = textureLoader.load(foamMacroTexUrl)
- foamMacroTexture.wrapS = THREE.RepeatWrapping
- foamMacroTexture.wrapT = THREE.RepeatWrapping
- const maskTexture = textureLoader.load(maskTexUrl)
- maskTexture.wrapS = THREE.RepeatWrapping
- maskTexture.wrapT = THREE.RepeatWrapping
- const flowNormalTexture = textureLoader.load(flowNormalTexUrl)
- flowNormalTexture.wrapS = THREE.RepeatWrapping
- flowNormalTexture.wrapT = THREE.RepeatWrapping
- const foamMacroNormalTexture = textureLoader.load(foamMacroNormalTexUrl)
- foamMacroNormalTexture.wrapS = THREE.RepeatWrapping
- foamMacroNormalTexture.wrapT = THREE.RepeatWrapping
- flowMaterial = createWaterFlowMaterial({
- flowTexture,
- foamMacroTexture,
- maskTexture,
- flowNormalTexture,
- foamMacroNormalTexture,
- })
- syncFlowParams()
- const loader = new GLTFLoader()
- loader.load(langGLBUrl, (gltf) => {
- const object = gltf.scene
- object.traverse((child) => {
- if ((child as THREE.Mesh).isMesh) {
- const mesh = child as THREE.Mesh
- mesh.material = flowMaterial!
- mesh.castShadow = true
- mesh.receiveShadow = true
- mesh.frustumCulled = false
- mesh.renderOrder = 1
- flowMesh = mesh
- }
- })
- const t = modelTransformMap.flow
- object.position.set(t.positionX, t.positionY, t.positionZ)
- object.rotation.order = 'YXZ'
- object.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- object.scale.set(t.scaleX, t.scaleY, t.scaleZ)
- object.name = 'flow'
- scene.add(object)
- modelList['flow'] = object
- materialList['flow'] = flowMaterial
- })
- }
- // 加载 CSCwater.glb 模型
- function loadCSCWaterModel() {
- cscwaterMaterial = StylizedWaterMaterial.clone()
- cscwaterMaterial.uniforms = THREE.UniformsUtils.clone(StylizedWaterMaterial.uniforms)
- syncCscwaterParams()
- const loader = new GLTFLoader()
- loader.load(cscwaterGLBUrl, (gltf) => {
- const object = gltf.scene
- object.traverse((child) => {
- if ((child as THREE.Mesh).isMesh) {
- const mesh = child as THREE.Mesh
- mesh.material = cscwaterMaterial!
- mesh.castShadow = true
- mesh.receiveShadow = true
- mesh.frustumCulled = false
- mesh.renderOrder = 1
- }
- })
- const t = modelTransformMap.cscwater
- object.position.set(t.positionX, t.positionY, t.positionZ)
- object.rotation.order = 'YXZ'
- object.rotation.set(
- THREE.MathUtils.degToRad(t.rotationX),
- THREE.MathUtils.degToRad(t.rotationY),
- THREE.MathUtils.degToRad(t.rotationZ)
- )
- object.scale.set(t.scaleX, t.scaleY, t.scaleZ)
- object.name = 'cscwater'
- scene.add(object)
- modelList['cscwater'] = object
- cscwaterModel = object
- materialList['cscwater'] = cscwaterMaterial
- })
- }
- // 加载超图 3D Tiles 瓦片数据(大范围三维场景)
- async function load3DTiles() {
- const tilesetUrl = props.tilesetUrl
- tilesRenderer = new SuperMapTilesRenderer(tilesetUrl)
- tilesRenderer.setCamera(camera)
- tilesRenderer.setResolutionFromRenderer(camera, renderer)
- tilesRenderer.registerPlugin(new ReorientationPlugin())
- scene.add(tilesRenderer.group)
- scene.fog = null
- tilesRenderer.addEventListener('load-error', (evt) => {
- console.error('3D Tiles load error:', evt.error, 'URL:', evt.url)
- })
- tilesRenderer.addEventListener('load-tile', (evt) => {
- console.log('Tile loaded:', evt.tile.content?.uri)
- })
- tilesRenderer.addEventListener('error-tile', (evt) => {
- console.error('Tile error:', evt.tile.content?.uri, evt.error)
- })
- console.log('3D Tiles renderer initialized:', tilesetUrl)
- }
- // 释放所有 Three.js 资源(可在 initScene 之前和 onUnmounted 时反复调用)
- function disposeScene() {
- cancelAnimationFrame(animationId)
- animationId = 0
- labelDataList.forEach(d => d.componentRef.value?.dispose())
- if (tilesRenderer) {
- if (scene) scene.remove(tilesRenderer.group)
- tilesRenderer.dispose?.()
- tilesRenderer = null
- }
- if (foamMesh) {
- if (scene) scene.remove(foamMesh)
- foamMesh.geometry.dispose()
- if (foamMaterial) {
- for (const key of Object.keys(foamMaterial.uniforms)) {
- const val = foamMaterial.uniforms[key].value
- if (val instanceof THREE.Texture) val.dispose()
- }
- foamMaterial.dispose()
- }
- foamMesh = null
- foamMaterial = null
- }
- if (flowMesh) {
- const parent = flowMesh.parent
- if (parent && scene) scene.remove(parent)
- flowMesh.geometry.dispose()
- if (flowMaterial) {
- for (const key of Object.keys(flowMaterial.uniforms)) {
- const val = flowMaterial.uniforms[key].value
- if (val instanceof THREE.Texture) val.dispose()
- }
- flowMaterial.dispose()
- }
- flowMesh = null
- flowMaterial = null
- }
- if (cscwaterModel) {
- if (scene) scene.remove(cscwaterModel)
- cscwaterModel.traverse((child) => {
- const mesh = child as THREE.Mesh
- if (mesh.isMesh) {
- mesh.geometry?.dispose()
- if (Array.isArray(mesh.material)) {
- mesh.material.forEach(m => m.dispose())
- } else {
- mesh.material?.dispose()
- }
- }
- })
- cscwaterModel = null
- }
- if (cscwaterMaterial) {
- cscwaterMaterial.dispose()
- cscwaterMaterial = null
- }
- if (waterMesh) {
- if (scene) scene.remove(waterMesh)
- waterMesh.geometry.dispose()
- waterMesh = null
- }
- if (water2Mesh) {
- if (scene) scene.remove(water2Mesh)
- water2Mesh.geometry.dispose()
- water2Mesh = null
- }
- if (StylizedWaterMaterial) {
- StylizedWaterMaterial.dispose()
- }
- if (sky) {
- if (scene) scene.remove(sky)
- sky.material.dispose()
- sky = null
- }
- if (depthRenderTarget) {
- depthRenderTarget.depthTexture?.dispose()
- depthRenderTarget.dispose()
- depthRenderTarget = null
- }
- if (controls) {
- controls.dispose()
- controls = null
- }
- if (renderer) {
- const domEl = renderer.domElement
- if (domEl && domEl.parentNode) {
- domEl.parentNode.removeChild(domEl)
- }
- renderer.forceContextLoss()
- renderer.dispose()
- renderer = null
- }
- if (scene) {
- while (scene.children.length > 0) {
- scene.remove(scene.children[0])
- }
- scene = null
- }
- camera = null
- sceneInitialized = false
- }
- // 初始化整个 Three.js 场景(带防重入保护)
- function initScene() {
- if (sceneInitialized) {
- disposeScene()
- }
- const container = containerRef.value!
- scene = new THREE.Scene()
- camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100000)
- camera.position.set(props.cameraPosition.x, props.cameraPosition.y, props.cameraPosition.z)
- renderer = new THREE.WebGLRenderer({ antialias: true })
- renderer.setSize(container.clientWidth, container.clientHeight)
- renderer.domElement.style.width = '100%'
- renderer.domElement.style.height = '100%'
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
- renderer.toneMapping = THREE.ACESFilmicToneMapping
- renderer.toneMappingExposure = 1.0
- renderer.shadowMap.enabled = true
- renderer.shadowMap.type = THREE.PCFShadowMap
- container.appendChild(renderer.domElement)
- const pixelWidth = Math.floor(container.clientWidth * window.devicePixelRatio)
- const pixelHeight = Math.floor(container.clientHeight * window.devicePixelRatio)
- depthRenderTarget = new THREE.WebGLRenderTarget(pixelWidth, pixelHeight)
- depthRenderTarget.depthTexture = new THREE.DepthTexture(pixelWidth, pixelHeight)
- controls = new OrbitControls(camera, renderer.domElement)
- controls.enableDamping = true
- controls.dampingFactor = 0.03
- controls.screenSpacePanning = false
- controls.minDistance = 0
- controls.maxDistance = Infinity
- controls.mouseButtons = {
- LEFT: THREE.MOUSE.PAN,
- MIDDLE: THREE.MOUSE.DOLLY,
- RIGHT: THREE.MOUSE.ROTATE,
- }
- controls.target.set(props.cameraTarget.x, props.cameraTarget.y, props.cameraTarget.z)
- controls.maxPolarAngle = Math.PI / 2.1
- controls.minDistance = 5
- controls.maxDistance = 500
- createSky()
- const hemisphereLight = new THREE.HemisphereLight(0xd4d4d4, 0x3d6b4a, 0.6)
- scene.add(hemisphereLight)
- sunDirection = new THREE.Vector3(20, 30, 10).normalize()
- const sunLight = new THREE.DirectionalLight(0xffeedd, 2.0)
- sunLight.position.set(20, 30, 10)
- sunLight.castShadow = true
- sunLight.shadow.mapSize.width = 2048
- sunLight.shadow.mapSize.height = 2048
- sunLight.shadow.camera.near = 0.5
- sunLight.shadow.camera.far = 60
- sunLight.shadow.camera.left = -20
- sunLight.shadow.camera.right = 20
- sunLight.shadow.camera.top = 20
- sunLight.shadow.camera.bottom = -20
- scene.add(sunLight)
- createWaterSurface()
- createWater2Surface()
- createWaterFoamSurface()
- loadWaterFlowModel()
- loadCSCWaterModel()
- load3DTiles()
- labelDataList.forEach(d => d.componentRef.value?.init(scene, camera))
- initRaycaster()
- sceneInitialized = true
- animate()
- }
- // 初始化鼠标射线拾取
- function initRaycaster() {
- raycaster = new THREE.Raycaster()
- mouse = new THREE.Vector2()
- const container = containerRef.value!
- container.addEventListener('click', onMouseClick)
- }
- // 鼠标点击获取 3D 场景中的坐标
- function onMouseClick(event: MouseEvent) {
- const target = event.target as HTMLElement
- if (target.closest('.panel') || target.closest('.toolbar')) return
- const container = containerRef.value!
- const rect = container.getBoundingClientRect()
- mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
- mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
- raycaster.setFromCamera(mouse, camera)
- const intersectObjects: THREE.Object3D[] = []
- if (tilesRenderer?.group) {
- intersectObjects.push(tilesRenderer.group)
- }
- if (waterMesh) {
- intersectObjects.push(waterMesh)
- }
- const intersects = raycaster.intersectObjects(intersectObjects, true)
- if (intersects.length > 0) {
- const point = intersects[0].point
- pickedPosition.value = {
- x: Number(point.x.toFixed(3)),
- y: Number(point.y.toFixed(3)),
- z: Number(point.z.toFixed(3)),
- }
- }
- }
- // 每帧渲染循环
- function animate() {
- animationId = requestAnimationFrame(animate)
- sky.material.uniforms.time.value += 0.001
- controls.update()
- tilesRenderer?.update()
- // 将水面的深度写入深度纹理(用于水面透明交互动效)
- waterMesh.visible = false
- const prevRT = renderer.getRenderTarget()
- renderer.setRenderTarget(depthRenderTarget)
- renderer.render(scene, camera)
- renderer.setRenderTarget(prevRT)
- waterMesh.visible = true
- // 更新各材质的时间 uniform 实现动画
- StylizedWaterMaterial.uniforms.depthSampler.value = depthRenderTarget.depthTexture
- StylizedWaterMaterial.uniforms.iTime.value += 0.016
- StylizedWaterMaterial.uniforms.collisionFoamTime.value += 0.016
- if (foamMaterial) {
- foamMaterial.uniforms.uTime.value += 0.016
- foamMaterial.uniforms.uCameraPos.value.copy(camera.position)
- }
- if (flowMaterial) {
- flowMaterial.uniforms.uTime.value += 0.016
- }
- if (cscwaterMaterial) {
- cscwaterMaterial.uniforms.depthSampler.value = depthRenderTarget.depthTexture
- cscwaterMaterial.uniforms.iTime.value += 0.016
- cscwaterMaterial.uniforms.collisionFoamTime.value += 0.016
- cscwaterMaterial.uniforms.cameraPos.value.copy(camera.position)
- }
- labelDataList.forEach(d => d.componentRef.value?.tick())
- StylizedWaterMaterial.uniforms.cameraPos.value.copy(camera.position)
- // 更新相机信息面板
- cameraInfo.value.position = {
- x: Number(camera.position.x.toFixed(3)),
- y: Number(camera.position.y.toFixed(3)),
- z: Number(camera.position.z.toFixed(3)),
- }
- cameraInfo.value.target = {
- x: Number(controls.target.x.toFixed(3)),
- y: Number(controls.target.y.toFixed(3)),
- z: Number(controls.target.z.toFixed(3)),
- }
- cameraInfo.value.distance = Number(camera.position.distanceTo(controls.target).toFixed(3))
- cameraInfo.value.minDistance = controls.minDistance
- cameraInfo.value.maxDistance = controls.maxDistance
- renderer.render(scene, camera)
- }
- // 窗口大小变化自适应
- function onResize() {
- if (!containerRef.value) return
- const width = containerRef.value.clientWidth
- const height = containerRef.value.clientHeight
- camera.aspect = width / height
- camera.updateProjectionMatrix()
- renderer.setSize(width, height)
- const pixelWidth = Math.floor(width * window.devicePixelRatio)
- const pixelHeight = Math.floor(height * window.devicePixelRatio)
- depthRenderTarget.setSize(pixelWidth, pixelHeight)
- StylizedWaterMaterial.uniforms.iResolution.value.set(width, height)
- if (tilesRenderer) {
- tilesRenderer.setResolutionFromRenderer(camera, renderer)
- }
- }
- onMounted(() => {
- initScene()
- window.addEventListener('resize', onResize)
- })
- // 组件销毁时释放所有 Three.js 资源
- onUnmounted(() => {
- window.removeEventListener('resize', onResize)
- if (containerRef.value) {
- containerRef.value.removeEventListener('click', onMouseClick)
- }
- disposeScene()
- })
- // ========== 材质参数的响应式监听,实时同步到着色器 ==========
- function syncWaterParams() {
- if (!StylizedWaterMaterial || !StylizedWaterMaterial.uniforms) return
- const p = waterParams.value
- StylizedWaterMaterial.uniforms.alpha.value = p.alpha
- StylizedWaterMaterial.uniforms.flowSpeed.value = p.flowSpeed
- StylizedWaterMaterial.uniforms.flowDirection.value.set(p.flowDirectionX, p.flowDirectionY)
- StylizedWaterMaterial.uniforms.normalRotation.value = p.normalRotation
- StylizedWaterMaterial.uniforms.waveHeight.value = p.waveHeight
- StylizedWaterMaterial.uniforms.shallowColor.value.set(p.waterColor)
- StylizedWaterMaterial.uniforms.deepColor.value.set(p.deepColor)
- StylizedWaterMaterial.uniforms.foamIntensity.value = p.foamIntensity
- StylizedWaterMaterial.uniforms.specIntensity.value = p.specIntensity
- StylizedWaterMaterial.uniforms.specPower.value = p.specPower
- StylizedWaterMaterial.uniforms.fresnelPower.value = p.fresnelPower
- StylizedWaterMaterial.uniforms.fresnelIntensity.value = p.fresnelIntensity
- StylizedWaterMaterial.uniforms.depthRange.value = p.depthRange
- StylizedWaterMaterial.uniforms.waterNormalStrength.value = p.waterNormalStrength
- StylizedWaterMaterial.uniforms.waterNormalTiling.value = p.waterNormalTiling
- StylizedWaterMaterial.uniforms.collisionFoamThreshold.value = p.collisionFoamThreshold
- StylizedWaterMaterial.uniforms.collisionFoamStrength.value = p.collisionFoamStrength
- StylizedWaterMaterial.uniforms.fresnelDistanceNear.value = p.fresnelDistanceNear
- StylizedWaterMaterial.uniforms.fresnelDistanceFar.value = p.fresnelDistanceFar
- }
- watch(() => waterParams.value, syncWaterParams, { deep: true })
- syncWaterParams()
- watch(() => cscwaterParams.value, () => {
- syncCscwaterParams()
- }, { deep: true })
- watch(() => foamMatParams.value, (p) => {
- if (!foamMaterial) return
- foamMaterial.uniforms.uColour.value.set(p.colour)
- foamMaterial.uniforms.uOpacity.value = p.opacity
- foamMaterial.uniforms.uWaterfallSpeed.value = p.waterfallSpeed
- foamMaterial.uniforms.uEdgeMaskTiling.value = p.edgeMaskTiling
- foamMaterial.uniforms.uEdgeMaskSpeed.value = p.edgeMaskSpeed
- foamMaterial.uniforms.uFresnelExponent.value = p.fresnelExponent
- foamMaterial.uniforms.uDirectionalFoamIntensity.value = p.directionalFoamIntensity
- foamMaterial.uniforms.uDirectionalFoamContrast.value = p.directionalFoamContrast
- foamMaterial.uniforms.uDirectionalFoam1Intensity.value = p.directionalFoam1Intensity
- foamMaterial.uniforms.uDirectionalFoam2Intensity.value = p.directionalFoam2Intensity
- foamMaterial.uniforms.uDirectionalFoam2Tiling.value = p.directionalFoam2Tiling
- foamMaterial.uniforms.uDirectionalFoam2Speed.value = p.directionalFoam2Speed
- foamMaterial.uniforms.uDirectionalFoam3Intensity.value = p.directionalFoam3Intensity
- foamMaterial.uniforms.uFoamFalloff.value = p.foamFalloff
- foamMaterial.uniforms.uGradientTop.value = p.gradientTop
- foamMaterial.uniforms.uGradientBottom.value = p.gradientBottom
- foamMaterial.uniforms.uGradientPower.value = p.gradientPower
- }, { deep: true })
- watch(() => flowParams.value, () => {
- syncFlowParams()
- }, { deep: true })
- watch(() => props.labelValues, (levels) => {
- for (const [id, value] of Object.entries(levels)) {
- if (labelValues[id] !== undefined) {
- labelValues[id] = value
- }
- }
- }, { deep: true })
- // ==================== 对外暴露的 API(供父组件/外部系统调用)====================
- function setLabelValue(id: string, value: number) {
- labelValues[id] = value
- }
- function setLabelValues(levels: Record<string, number>) {
- for (const [id, value] of Object.entries(levels)) {
- labelValues[id] = value
- }
- }
- function flyTo(
- position: { x: number; y: number; z: number },
- target?: { x: number; y: number; z: number },
- durationMs: number = 1500,
- ) {
- const startPos = camera.position.clone()
- const endPos = new THREE.Vector3(position.x, position.y, position.z)
- const startTarget = controls.target.clone()
- const endTarget = target
- ? new THREE.Vector3(target.x, target.y, target.z)
- : controls.target.clone()
- const startTime = performance.now()
- function animateFly() {
- const elapsed = performance.now() - startTime
- const t = Math.min(elapsed / durationMs, 1)
- const ease = t * t * (3 - 2 * t)
- camera.position.lerpVectors(startPos, endPos, ease)
- controls.target.lerpVectors(startTarget, endTarget, ease)
- controls.update()
- if (t < 1) requestAnimationFrame(animateFly)
- }
- animateFly()
- }
- function flyToPreset(presetId: string, durationMs: number = 1500) {
- const preset = cameraPresets.find(p => p.id === presetId)
- if (!preset) {
- console.warn(`Camera preset "${presetId}" not found`)
- return
- }
- flyTo(
- { x: preset.positionX, y: preset.positionY, z: preset.positionZ },
- { x: preset.targetX, y: preset.targetY, z: preset.targetZ },
- durationMs,
- )
- }
- defineExpose({
- labelDataList,
- labelValues,
- setLabelValue,
- setLabelValues,
- flyTo,
- flyToPreset,
- cameraPresets,
- })
- </script>
- <template>
- <!-- 3D 渲染容器 -->
- <div ref="containerRef" class="scene-container" />
- <!-- 水位标签(Sprite + 指针) -->
- <WaterLevelLabel
- v-for="data in labelDataList"
- :key="data.config.id"
- :ref="(el: any) => { if (el) data.componentRef.value = el }"
- :label-id="data.config.id"
- :type="data.config.type"
- :position-x="data.config.positionX"
- :position-y="data.config.positionY"
- :position-z="data.config.positionZ"
- :value="labelValues[data.config.id]"
- />
- <!-- 右上角调试工具栏 -->
- <div v-if="props.showDebugTools" class="toolbar">
- <button class="toolbar-btn" :class="{ active: showCoordinatePanel }" @click="showCoordinatePanel = !showCoordinatePanel">坐标</button>
- <button class="toolbar-btn" :class="{ active: showCameraPanel }" @click="showCameraPanel = !showCameraPanel">相机</button>
- <button class="toolbar-btn" :class="{ active: showModelPanel }" @click="showModelPanel = !showModelPanel">模型</button>
- <button class="toolbar-btn" :class="{ active: showMaterialPanel }" @click="showMaterialPanel = !showMaterialPanel">材质</button>
- <button class="toolbar-btn" :class="{ active: showCameraPresetPanel }" @click="showCameraPresetPanel = !showCameraPresetPanel">视角</button>
- </div>
- <!-- 拾取坐标面板 -->
- <div v-if="props.showDebugTools && showCoordinatePanel && pickedPosition" class="panel coordinate-panel">
- <div class="panel-header">
- <span class="panel-title">拾取坐标 (m)</span>
- <button class="toggle-btn" @click="showCoordinatePanel = false">×</button>
- </div>
- <div class="coordinate-item">
- <span class="coordinate-label">X:</span>
- <span class="coordinate-value">{{ pickedPosition.x }}</span>
- </div>
- <div class="coordinate-item">
- <span class="coordinate-label">Y:</span>
- <span class="coordinate-value">{{ pickedPosition.y }}</span>
- </div>
- <div class="coordinate-item">
- <span class="coordinate-label">Z:</span>
- <span class="coordinate-value">{{ pickedPosition.z }}</span>
- </div>
- </div>
- <!-- 相机信息面板 -->
- <div v-if="props.showDebugTools && showCameraPanel" class="panel camera-panel">
- <div class="panel-header">
- <span class="panel-title">相机信息</span>
- <button class="toggle-btn" @click="showCameraPanel = false">×</button>
- </div>
- <div class="camera-section">
- <div class="section-label">位置 (m)</div>
- <div class="camera-item">
- <span class="camera-label">X:</span>
- <span class="camera-value">{{ cameraInfo.position.x }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Y:</span>
- <span class="camera-value">{{ cameraInfo.position.y }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Z:</span>
- <span class="camera-value">{{ cameraInfo.position.z }}</span>
- </div>
- </div>
- <div class="camera-section">
- <div class="section-label">目标点 (m)</div>
- <div class="camera-item">
- <span class="camera-label">X:</span>
- <span class="camera-value">{{ cameraInfo.target.x }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Y:</span>
- <span class="camera-value">{{ cameraInfo.target.y }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Z:</span>
- <span class="camera-value">{{ cameraInfo.target.z }}</span>
- </div>
- </div>
- <div class="camera-section">
- <div class="section-label">缩放距离 (m)</div>
- <div class="camera-item">
- <span class="camera-label">当前:</span>
- <span class="camera-value">{{ cameraInfo.distance }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">最近:</span>
- <span class="camera-value">{{ cameraInfo.minDistance }}m</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">最远:</span>
- <span class="camera-value">{{ cameraInfo.maxDistance }}m</span>
- </div>
- </div>
- </div>
- <!-- 模型变换调试面板 -->
- <div v-if="props.showDebugTools && showModelPanel" class="panel model-panel">
- <div class="panel-header">
- <span class="panel-title">模型变换</span>
- <button class="toggle-btn" @click="showModelPanel = false">×</button>
- </div>
- <div class="model-section">
- <div class="section-label">选择模型</div>
- <select v-model="selectedModelKey" class="model-select">
- <option value="water">水面</option>
- <option value="water2">水面2</option>
- <option value="foam">泡沫片面</option>
- <option value="flow">流动纹理模型</option>
- <option value="cscwater">CSCwater</option>
- </select>
- </div>
- <div class="model-section">
- <div class="section-label">位置</div>
- <div class="input-item">
- <span class="input-label">X</span>
- <input type="number" v-model.number="modelTransform.positionX" step="0.1" class="number-input" />
- </div>
- <div class="input-item">
- <span class="input-label">Y</span>
- <input type="number" v-model.number="modelTransform.positionY" step="0.1" class="number-input" />
- </div>
- <div class="input-item">
- <span class="input-label">Z</span>
- <input type="number" v-model.number="modelTransform.positionZ" step="0.1" class="number-input" />
- </div>
- </div>
- <div class="model-section">
- <div class="section-label">旋转</div>
- <div class="input-item">
- <span class="input-label">X (°)</span>
- <input type="number" v-model.number="modelTransform.rotationX" step="1" class="number-input" />
- </div>
- <div class="input-item">
- <span class="input-label">Y (°)</span>
- <input type="number" v-model.number="modelTransform.rotationY" step="1" class="number-input" />
- </div>
- <div class="input-item">
- <span class="input-label">Z (°)</span>
- <input type="number" v-model.number="modelTransform.rotationZ" step="1" class="number-input" />
- </div>
- </div>
- <div class="model-section">
- <div class="section-label">缩放</div>
- <div class="input-item">
- <span class="input-label">X</span>
- <input type="number" v-model.number="modelTransform.scaleX" step="0.5" class="number-input" />
- </div>
- <div class="input-item">
- <span class="input-label">Y</span>
- <input type="number" v-model.number="modelTransform.scaleY" step="0.5" class="number-input" />
- </div>
- <div class="input-item">
- <span class="input-label">Z</span>
- <input type="number" v-model.number="modelTransform.scaleZ" step="0.5" class="number-input" />
- </div>
- </div>
- <div class="model-section">
- <button class="default-btn" @click="saveModelDefaults">设置默认值</button>
- </div>
- </div>
- <!-- 材质参数调试面板 -->
- <div v-if="props.showDebugTools && showMaterialPanel" class="panel material-panel">
- <div class="panel-header">
- <span class="panel-title">材质参数</span>
- <button class="toggle-btn" @click="showMaterialPanel = false">×</button>
- </div>
- <div class="material-section">
- <div class="section-label">选择材质</div>
- <select v-model="selectedMaterialKey" class="model-select">
- <option value="water">水材质</option>
- <option value="foam">泡沫材质</option>
- <option value="flow">流动纹理材质</option>
- <option value="cscwater">CSCwater 材质</option>
- </select>
- </div>
- <template v-if="selectedMaterialKey === 'water'">
- <div class="material-section">
- <div class="section-label">透明度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.alpha" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ waterParams.alpha.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">浪高</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.waveHeight" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ waterParams.waveHeight.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水流速度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.flowSpeed" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ waterParams.flowSpeed.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">流向 X</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.flowDirectionX" min="-2" max="2" step="0.1" />
- <span class="slider-value">{{ waterParams.flowDirectionX.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">流向 Z</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.flowDirectionY" min="-2" max="2" step="0.1" />
- <span class="slider-value">{{ waterParams.flowDirectionY.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">法线旋转</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.normalRotation" min="-180" max="180" step="1" />
- <span class="slider-value">{{ waterParams.normalRotation.toFixed(0) }}°</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">浅水颜色</div>
- <div class="color-item">
- <input type="color" v-model="waterParams.waterColor" />
- <span class="color-value">{{ waterParams.waterColor }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">深水颜色</div>
- <div class="color-item">
- <input type="color" v-model="waterParams.deepColor" />
- <span class="color-value">{{ waterParams.deepColor }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水深范围</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.depthRange" min="1" max="50" step="0.5" />
- <span class="slider-value">{{ waterParams.depthRange.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水法线强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.waterNormalStrength" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ waterParams.waterNormalStrength.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水纹平铺</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.waterNormalTiling" min="0.01" max="5" step="0.01" />
- <span class="slider-value">{{ waterParams.waterNormalTiling.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">高光强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.specIntensity" min="0" max="5" step="0.1" />
- <span class="slider-value">{{ waterParams.specIntensity.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">高光锐度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.specPower" min="1" max="256" step="1" />
- <span class="slider-value">{{ waterParams.specPower.toFixed(0) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">菲涅尔功率</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.fresnelPower" min="0.1" max="10" step="0.1" />
- <span class="slider-value">{{ waterParams.fresnelPower.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">菲涅尔强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.fresnelIntensity" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ waterParams.fresnelIntensity.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">泡沫强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.foamIntensity" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ waterParams.foamIntensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">碰撞泡沫阈值</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.collisionFoamThreshold" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ waterParams.collisionFoamThreshold.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">碰撞泡沫强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.collisionFoamStrength" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ waterParams.collisionFoamStrength.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <button class="default-btn" @click="saveWaterDefaults">设置默认值</button>
- </div>
- </template>
- <template v-if="selectedMaterialKey === 'foam'">
- <div class="material-section">
- <div class="section-label">颜色</div>
- <div class="color-item">
- <input type="color" v-model="foamMatParams.colour" />
- <span class="color-value">{{ foamMatParams.colour }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">不透明度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.opacity" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ foamMatParams.opacity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">瀑布流速</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.waterfallSpeed" min="0" max="2" step="0.01" />
- <span class="slider-value">{{ foamMatParams.waterfallSpeed.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">菲涅尔指数</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.fresnelExponent" min="0" max="20" step="0.1" />
- <span class="slider-value">{{ foamMatParams.fresnelExponent.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">边缘遮罩平铺</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.edgeMaskTiling" min="0.1" max="2" step="0.01" />
- <span class="slider-value">{{ foamMatParams.edgeMaskTiling.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">边缘遮罩速度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.edgeMaskSpeed" min="0" max="10" step="0.1" />
- <span class="slider-value">{{ foamMatParams.edgeMaskSpeed.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">方向泡沫强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoamIntensity" min="0" max="5" step="0.01" />
- <span class="slider-value">{{ foamMatParams.directionalFoamIntensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">方向泡沫对比度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoamContrast" min="0.1" max="10" step="0.1" />
- <span class="slider-value">{{ foamMatParams.directionalFoamContrast.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">层1强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoam1Intensity" min="0" max="5" step="0.01" />
- <span class="slider-value">{{ foamMatParams.directionalFoam1Intensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">层2强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoam2Intensity" min="0" max="5" step="0.01" />
- <span class="slider-value">{{ foamMatParams.directionalFoam2Intensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">层2平铺</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoam2Tiling" min="0.1" max="5" step="0.01" />
- <span class="slider-value">{{ foamMatParams.directionalFoam2Tiling.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">层2速度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoam2Speed" min="0" max="50" step="0.5" />
- <span class="slider-value">{{ foamMatParams.directionalFoam2Speed.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">层3强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.directionalFoam3Intensity" min="0" max="5" step="0.01" />
- <span class="slider-value">{{ foamMatParams.directionalFoam3Intensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">泡沫衰减</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.foamFalloff" min="0.1" max="10" step="0.1" />
- <span class="slider-value">{{ foamMatParams.foamFalloff.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">渐变顶部</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.gradientTop" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ foamMatParams.gradientTop.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">渐变底部</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.gradientBottom" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ foamMatParams.gradientBottom.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">渐变曲线</div>
- <div class="slider-item">
- <input type="range" v-model.number="foamMatParams.gradientPower" min="0.1" max="5" step="0.1" />
- <span class="slider-value">{{ foamMatParams.gradientPower.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <button class="default-btn" @click="saveFoamDefaults">设置默认值</button>
- </div>
- </template>
- <template v-if="selectedMaterialKey === 'flow'">
- <div class="material-section">
- <div class="section-label">颜色</div>
- <div class="color-item">
- <input type="color" v-model="flowParams.colour" />
- <span class="color-value">{{ flowParams.colour }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">流动速度 X</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.speedX" min="-2" max="2" step="0.001" />
- <span class="slider-value">{{ flowParams.speedX.toFixed(3) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">流动速度 Y</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.speedY" min="-2" max="2" step="0.001" />
- <span class="slider-value">{{ flowParams.speedY.toFixed(3) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">平铺 U</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.tilingU" min="-5" max="5" step="0.01" />
- <span class="slider-value">{{ flowParams.tilingU.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">平铺 V</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.tilingV" min="-5" max="5" step="0.01" />
- <span class="slider-value">{{ flowParams.tilingV.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">旋转角度</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.rotationAngle" min="-1" max="1" step="0.01" />
- <span class="slider-value">{{ flowParams.rotationAngle.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">泡沫强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.power" min="0" max="5" step="0.01" />
- <span class="slider-value">{{ flowParams.power.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水闸强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.waterGate" min="0" max="200" step="0.5" />
- <span class="slider-value">{{ flowParams.waterGate.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水闸翻转</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.waterGate02" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ flowParams.waterGate02.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">边缘遮罩强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.edgeMaskIntensity" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ flowParams.edgeMaskIntensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">不透明度功率</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.opaquePower" min="0" max="20" step="0.01" />
- <span class="slider-value">{{ flowParams.opaquePower.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">边缘渐变淡出</div>
- <div class="slider-item">
- <input type="range" v-model.number="flowParams.edgeFade" min="0" max="0.5" step="0.01" />
- <span class="slider-value">{{ flowParams.edgeFade.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <button class="default-btn" @click="saveFlowDefaults">设置默认值</button>
- </div>
- </template>
- <template v-if="selectedMaterialKey === 'cscwater'">
- <div class="material-section">
- <div class="section-label">透明度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.alpha" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ cscwaterParams.alpha.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">浪高</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.waveHeight" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ cscwaterParams.waveHeight.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水流速度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.flowSpeed" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ cscwaterParams.flowSpeed.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">流向 X</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.flowDirectionX" min="-2" max="2" step="0.1" />
- <span class="slider-value">{{ cscwaterParams.flowDirectionX.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">流向 Z</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.flowDirectionY" min="-2" max="2" step="0.1" />
- <span class="slider-value">{{ cscwaterParams.flowDirectionY.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">法线旋转</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.normalRotation" min="-180" max="180" step="1" />
- <span class="slider-value">{{ cscwaterParams.normalRotation.toFixed(0) }}°</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">浅水颜色</div>
- <div class="color-item">
- <input type="color" v-model="cscwaterParams.waterColor" />
- <span class="color-value">{{ cscwaterParams.waterColor }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">深水颜色</div>
- <div class="color-item">
- <input type="color" v-model="cscwaterParams.deepColor" />
- <span class="color-value">{{ cscwaterParams.deepColor }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水深范围</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.depthRange" min="1" max="50" step="0.5" />
- <span class="slider-value">{{ cscwaterParams.depthRange.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水法线强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.waterNormalStrength" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ cscwaterParams.waterNormalStrength.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">水纹平铺</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.waterNormalTiling" min="0.01" max="5" step="0.01" />
- <span class="slider-value">{{ cscwaterParams.waterNormalTiling.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">高光强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.specIntensity" min="0" max="5" step="0.1" />
- <span class="slider-value">{{ cscwaterParams.specIntensity.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">高光锐度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.specPower" min="1" max="256" step="1" />
- <span class="slider-value">{{ cscwaterParams.specPower.toFixed(0) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">菲涅尔功率</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.fresnelPower" min="0.1" max="10" step="0.1" />
- <span class="slider-value">{{ cscwaterParams.fresnelPower.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">菲涅尔强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.fresnelIntensity" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ cscwaterParams.fresnelIntensity.toFixed(1) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">泡沫强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.foamIntensity" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ cscwaterParams.foamIntensity.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">碰撞泡沫阈值</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.collisionFoamThreshold" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ cscwaterParams.collisionFoamThreshold.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <div class="section-label">碰撞泡沫强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="cscwaterParams.collisionFoamStrength" min="0" max="2" step="0.05" />
- <span class="slider-value">{{ cscwaterParams.collisionFoamStrength.toFixed(2) }}</span>
- </div>
- </div>
- <div class="material-section">
- <button class="default-btn" @click="saveCscwaterDefaults">设置默认值</button>
- </div>
- </template>
- </div>
- <!-- 视角选择面板 -->
- <div v-if="props.showDebugTools && showCameraPresetPanel" class="panel camera-preset-panel">
- <div class="panel-header">
- <span class="panel-title">视角选择</span>
- <button class="toggle-btn" @click="showCameraPresetPanel = false">×</button>
- </div>
- <div class="preset-list">
- <div
- v-for="preset in cameraPresets"
- :key="preset.id"
- class="preset-item"
- @click="showCameraPresetPanel = false; flyToPreset(preset.id)"
- >
- <span class="preset-name">{{ preset.name }}</span>
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- /* 全屏 3D 渲染容器 */
- .scene-container {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- overflow: hidden;
- }
- /* 右上角调试工具栏 */
- .toolbar {
- position: fixed;
- top: 20px;
- right: 20px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- z-index: 1001;
- }
- .toolbar-btn {
- padding: 10px 16px;
- border: none;
- border-radius: 8px;
- background: rgba(0, 0, 0, 0.85);
- color: #888;
- font-size: 13px;
- font-weight: bold;
- cursor: pointer;
- transition: all 0.2s;
- min-width: 70px;
- }
- .toolbar-btn:hover {
- background: rgba(30, 30, 30, 0.9);
- color: #fff;
- }
- .toolbar-btn.active {
- color: #fff;
- box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
- }
- .toolbar-btn:nth-child(1).active { color: #4fc3f7; }
- .toolbar-btn:nth-child(2).active { color: #ff9800; }
- .toolbar-btn:nth-child(3).active { color: #ff4081; }
- .toolbar-btn:nth-child(4).active { color: #4dd0e1; }
- .toolbar-btn:nth-child(5).active { color: #ffeb3b; }
- /* 调试面板通用样式 */
- .panel {
- position: fixed;
- top: 20px;
- left: 20px;
- background: rgba(0, 0, 0, 0.85);
- color: white;
- padding: 15px;
- border-radius: 8px;
- font-family: 'Courier New', monospace;
- z-index: 1000;
- max-height: calc(100vh - 40px);
- overflow-y: auto;
- }
- .coordinate-panel { min-width: 200px; }
- .camera-panel { min-width: 200px; }
- .model-panel { min-width: 260px; }
- .material-panel { min-width: 260px; }
- .camera-preset-panel {
- min-width: 200px;
- left: auto;
- right: 20px;
- top: auto;
- bottom: 20px;
- }
- .preset-list {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-top: 8px;
- }
- .preset-item {
- padding: 8px 12px;
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.08);
- color: #ccc;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.2s;
- }
- .preset-item:hover {
- background: rgba(255, 235, 59, 0.2);
- color: #fff;
- }
- .preset-name {
- font-family: 'Microsoft YaHei', sans-serif;
- }
- .panel-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.3);
- }
- .panel-title {
- font-weight: bold;
- font-size: 14px;
- }
- .coordinate-panel .panel-title { color: #4fc3f7; }
- .camera-panel .panel-title { color: #ff9800; }
- .model-panel .panel-title { color: #ff4081; }
- .material-panel .panel-title { color: #4dd0e1; }
- .camera-preset-panel .panel-title { color: #ffeb3b; }
- /* 关闭按钮 */
- .toggle-btn {
- width: 22px;
- height: 22px;
- border: none;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.15);
- color: white;
- font-size: 14px;
- line-height: 1;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.2s;
- }
- .toggle-btn:hover {
- background: rgba(255, 255, 255, 0.3);
- }
- /* 坐标列表 */
- .coordinate-item {
- display: flex;
- justify-content: space-between;
- margin: 5px 0;
- }
- .coordinate-label {
- color: #81c784;
- font-weight: bold;
- }
- .coordinate-value {
- color: #fff;
- }
- /* 相机信息分组 */
- .camera-section {
- margin-bottom: 10px;
- }
- .section-label {
- color: #81c784;
- font-size: 11px;
- margin-bottom: 5px;
- font-weight: bold;
- }
- .camera-item {
- display: flex;
- justify-content: space-between;
- margin: 3px 0;
- }
- .camera-label {
- color: #4fc3f7;
- }
- .camera-value {
- color: #fff;
- }
- /* 模型/材质分组 */
- .model-section,
- .material-section {
- margin-bottom: 12px;
- }
- .model-section .section-label,
- .material-section .section-label {
- color: #81c784;
- font-size: 11px;
- margin-bottom: 6px;
- font-weight: bold;
- }
- /* 下拉选择器 */
- .model-select {
- width: 100%;
- padding: 6px 8px;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.08);
- color: #fff;
- font-size: 13px;
- font-family: inherit;
- cursor: pointer;
- outline: none;
- }
- .model-select option {
- background: #222;
- color: #fff;
- }
- /* 滑条控件 */
- .slider-item {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 4px;
- }
- .slider-label {
- font-size: 12px;
- color: #aaa;
- min-width: 32px;
- }
- .slider-item input[type="range"] {
- flex: 1;
- height: 4px;
- -webkit-appearance: none;
- appearance: none;
- background: rgba(255, 255, 255, 0.15);
- border-radius: 2px;
- outline: none;
- }
- .slider-item input[type="range"]::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: #ff4081;
- cursor: pointer;
- }
- .slider-value {
- min-width: 50px;
- text-align: right;
- color: #fff;
- font-size: 11px;
- font-family: 'Consolas', monospace;
- }
- /* 数字输入框 */
- .input-item {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 4px;
- }
- .input-label {
- font-size: 12px;
- color: #aaa;
- min-width: 32px;
- }
- .number-input {
- flex: 1;
- padding: 4px 8px;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.08);
- color: #fff;
- font-size: 13px;
- font-family: 'Consolas', monospace;
- outline: none;
- }
- .number-input:focus {
- border-color: #ff4081;
- }
- /* 颜色拾取器 */
- .color-item {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .color-item input[type="color"] {
- width: 40px;
- height: 28px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- background: transparent;
- }
- .color-item input[type="color"]::-webkit-color-swatch-wrapper {
- padding: 0;
- }
- .color-item input[type="color"]::-webkit-color-swatch {
- border: none;
- border-radius: 4px;
- }
- .color-value {
- color: #fff;
- font-size: 11px;
- font-family: 'Consolas', monospace;
- }
- /* 材质面板滑条颜色(区别于模型的粉色,材质用青色) */
- .material-section .slider-item input[type="range"] {
- background: rgba(255, 255, 255, 0.15);
- }
- .material-section .slider-item input[type="range"]::-webkit-slider-thumb {
- background: #4dd0e1;
- }
- /* 保存默认值按钮 */
- .default-btn {
- width: 100%;
- padding: 8px 16px;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.08);
- color: #4dd0e1;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.2s;
- }
- .default-btn:hover {
- background: rgba(77, 208, 225, 0.15);
- border-color: #4dd0e1;
- }
- /* 底部提示消息 */
- .toast-message {
- position: fixed;
- bottom: 40px;
- left: 50%;
- transform: translateX(-50%);
- background: rgba(0, 0, 0, 0.85);
- color: #4dd0e1;
- padding: 10px 24px;
- border-radius: 8px;
- font-size: 14px;
- font-family: 'Microsoft YaHei', sans-serif;
- z-index: 9999;
- pointer-events: none;
- transition: opacity 0.3s ease;
- border: 1px solid rgba(77, 208, 225, 0.3);
- }
- .toast-message.toast-fade {
- opacity: 0;
- }
- </style>
|