用 React Three Fiber 实现 3D 城市模型的扩散光圈特效

本文介绍了如何使用 React Three Fiber(R3F)和 Three.js 实现一个从中心向外扩散的光圈特效(DiffuseAperture 组件),并将其集成到城市 3D 模型(CityModel 组件)中。该特效通过动态调整圆柱几何体的大小和透明度,模拟出类似水波扩散的视觉效果,可用于增强 3D 场景的交互反馈或氛围营造。我们将从功能原理、实现细节和集成方式三个方面,用通俗易懂的语言讲解技术要点。

 更多该系列更新请参考

Three.js 如何控制 GLB 模型的内置属性实现精准显示-CSDN博客

React Three Fiber 实现 3D 模型点击高亮交互的核心技巧-CSDN博客

在 React Three Fiber 中实现 3D 模型点击扩散波效果-CSDN博客

 什么是 “扩散光圈”?

想象一下,当你往平静的湖面扔一颗石子,水波会从落点向四周一圈圈扩散,逐渐变大并消失 —— 我们实现的 “扩散光圈” 就是这个效果的 3D 版本。在 CityModel 城市模型中,这个特效表现为:从城市中心(原点)向外扩散的环形光圈,随着时间推移,光圈半径不断增大,同时逐渐变得透明,最终消失;随后又从中心重新开始扩散,形成循环动画。

这个特效可以为 3D 城市模型增添动态感,比如:

  • 作为场景加载完成的 “开场动画”
  • 作为用户点击城市中心的交互反馈
  • 模拟信号覆盖、能量扩散等业务场景

 核心原理:用 “空心圆柱” 模拟光圈

实现这个特效的核心思路很简单:用一个 “没有上下底的空心圆柱” 作为光圈的载体,通过动态改变它的大小和透明度,让它看起来像在 “扩散消失”。

为什么用圆柱?

  • 圆柱的侧面是环形,天然适合模拟 “光圈”
  • 可以通过缩放控制半径(大小),通过透明度控制可见度
  • 隐藏上下底面后,只剩侧面,视觉上更像 “一圈光”

 实现细节:让光圈 “动” 起来的关键

1. 基础结构:构建空心圆柱

// 核心代码片段:创建圆柱几何体
<cylinderGeometryargs={[initialRadius, // 顶部半径(初始大小)initialRadius, // 底部半径(和顶部相同,保证是正圆)height, // 圆柱高度(光圈的“厚度”,越薄越像2D光圈)64, // 径向分段数(数值越大,光圈边缘越平滑)1, // 高度分段数(固定为1即可)true, // 开口(关键!让圆柱没有上下底,只剩侧面)]}
/>

通俗解释:就像用一张纸条卷成一个空心圆筒,然后把上下两个口剪掉,只剩中间的环形侧面 —— 这就是我们的 “光圈” 雏形。

2. 动态变大:让光圈 “扩散”

光圈的 “扩散” 效果,本质是让圆柱的半径随时间不断增大:

// 核心代码片段:每帧更新半径
useFrame(() => {// 1. 让半径随时间增大(expandSpeed 控制扩散快慢)radiusRef.current += expandSpeed * 0.016;// 用缩放控制大小(x和z轴同时放大,保证是正圆)apertureRef.current.scale.set(radiusRef.current, // x轴缩放1, // y轴不缩放(保持厚度不变)radiusRef.current, // z轴缩放);
});

通俗解释:想象圆筒被吹气球一样慢慢变大,而且是均匀地向四周扩张,就像水波越来越大。这里的 0.016 是为了适配屏幕刷新率(约 60 帧 / 秒),让不同设备上的扩散速度一致。

3. 逐渐消失:让光圈 “淡化”

光扩散的同时会慢慢变淡,通过降低材质的透明度实现:

// 核心代码片段:控制透明度
useFrame(() => {// 2. 让透明度随时间降低(fadeSpeed 控制消失快慢)opacityRef.current -= fadeSpeed * 0.016;material.opacity = Math.max(opacityRef.current, 0); // 透明度不小于0
});

通俗解释:就像用半透明的纸做的圆筒,随着扩散,纸张越来越薄,直到完全看不见。

4. 循环动画:让光圈 “重复扩散”

当光圈扩散到最大范围或完全消失后,需要重置状态重新开始:

// 核心代码片段:重置条件
if (radiusRef.current > maxRadius || opacityRef.current <= 0) {radiusRef.current = initialRadius; // 半径变回初始大小opacityRef.current = 1; // 透明度重置为完全可见
}

通俗解释:这就像设置了一个循环播放的 “水波” 动画,一波消失后,新的一波从中心重新开始扩散。

5. 纹理增强:让光圈更生动(可选)

如果想让光圈有纹路(比如光斑、条纹),可以添加纹理贴图:

// 核心代码片段:添加纹理
if (textureUrl) {const texture = textureLoader.load(textureUrl); // 加载纹理图片materialParams.map = texture; // 应用到材质上
}

通俗解释:就像给圆筒的侧面贴上带花纹的贴纸,让光圈看起来更有细节(比如模拟雷达扫描的波纹)。

光波组件代码(完整)

import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import { useRef, useMemo } from 'react'// 扩散光圈组件
export const DiffuseAperture = ({color = ' 0x4C8BF5', // 光圈颜色initialRadius = 0.5, // 初始半径maxRadius = 10, // 最大扩散半径expandSpeed = 2, // 扩散速度(半径增长速率)fadeSpeed = 0.8, // 淡出速度(透明度降低速率)textureUrl, // 侧面纹理贴图URL(可选)height = 1.5, // 从 0.1 增大到 1.5(根据场景比例调整)radialSegments = 128, // 径向分段数(从64增至128,边缘更平滑)heightSegments = 3, // 高度分段数(从1增至3,厚度方向有细节)
}: {color?: stringinitialRadius?: numbermaxRadius?: numberexpandSpeed?: numberfadeSpeed?: numberheight?: numbertextureUrl?: string
}) => {const apertureRef = useRef<THREE.Mesh>(null)const radiusRef = useRef(initialRadius) // 跟踪当前半径const opacityRef = useRef(1) // 跟踪当前透明度// 创建圆柱侧面材质(带纹理支持)const material = useMemo(() => {const textureLoader = new THREE.TextureLoader()const materialParams: THREE.MeshBasicMaterialParameters = {color: new THREE.Color(color),transparent: true, // 启用透明度side: THREE.DoubleSide, // 确保侧面可见}// 若提供纹理URL,加载纹理并应用if (textureUrl) {const texture = textureLoader.load(textureUrl)materialParams.map = texture}return new THREE.MeshBasicMaterial(materialParams)}, [color, textureUrl])// 每帧更新圆柱状态(半径增大+透明度降低)useFrame(() => {if (!apertureRef.current) return// 1. 更新半径(逐渐增大)radiusRef.current += expandSpeed * 0.016 // 基于帧率的平滑增长apertureRef.current.scale.set(radiusRef.current, // X轴缩放(控制半径)1, // Y轴不缩放(保持高度)radiusRef.current, // Z轴缩放(控制半径))// 2. 更新透明度(逐渐降低)opacityRef.current -= fadeSpeed * 0.016material.opacity = Math.max(opacityRef.current, 0) // 不小于0// 3. 当完全透明或超出最大半径时,重置状态(循环扩散)if (radiusRef.current > maxRadius || opacityRef.current <= 0) {radiusRef.current = initialRadiusopacityRef.current = 1}})return (<mesh ref={apertureRef}>{/* 圆柱几何体:顶面和底面隐藏,仅保留侧面 */}<cylinderGeometryargs={[initialRadius, // 顶部半径initialRadius, // 底部半径(与顶部相同,确保是正圆柱)height, // 圆柱高度(厚度)64, // 径向分段数(越高越平滑)1, // 高度分段数true, // 开口(无顶面和底面)]}/><primitive object={material} /></mesh>)
}

集成到城市模型:让特效 “服务于场景”

我们的扩散光圈不是孤立存在的,需要和城市模型(CityModel 组件)配合使用,才能发挥最大效果: 

1. 位置对齐:让光圈从城市中心扩散

// 在 CityModel 组件中使用 DiffuseAperture
return (<>{/* 城市模型(已居中到原点) */}<primitive object={scene} ref={modelRef} />{/* 扩散光圈:放在城市中心 */}<DiffuseAperturecolor="#4C8BF5"initialRadius={0.5}maxRadius={20} // 扩散范围适配城市大小rotation={[Math.PI/2, 0, 0]} // 旋转90度,让光圈与地面平行/></>
);

关键细节:城市模型通过 modelRef.current.position.sub(center) 已居中到原点(0,0,0),光圈默认也在原点,因此能从城市中心开始扩散。rotation 是为了让光圈 “躺平” 在地面上(默认是直立的)。

2. 参数适配:让特效和场景协调

参数作用适配城市模型的建议值
maxRadius光圈最大扩散范围设为城市宽度的 1.5 倍
height光圈厚度0.05-0.1(薄一点更自然)
expandSpeed扩散速度2-3(太快看不清,太慢拖沓)
color光圈颜色与城市主色调对比(如蓝色)

3. 增强体验:多光圈叠加

通过同时渲染多个参数不同的光圈,可以形成更丰富的效果:

// 多光圈叠加示例
<><DiffuseAperture color="#ff6b3b" maxRadius={15} /><DiffuseAperture color="#ffc154" maxRadius={25} expandSpeed={2.5} /><DiffuseAperture color="#609bdf" maxRadius={35} expandSpeed={3} />
</>

效果:就像同时往水里扔三颗石子,形成的水波一圈套一圈,颜色从内到外渐变(红→黄→蓝),增强层次感。

 完整组件引用代码

注释掉的部分是模型的点击高亮,现在为了演示效果,默认是初始化高亮的

import { useGLTF } from '@react-three/drei'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { useModelManager } from '../../../utils/viewHelper/viewContext'
import { DiffuseAperture } from '../../WaveEffect'export const CityModel = ({ url }: { url: string }) => {const { scene } = useGLTF(url)const modelRef = useRef<THREE.Group>(null)const helper = useModelManager()const { camera } = useThree()// const raycaster = useRef(new THREE.Raycaster())// const pointer = useRef(new THREE.Vector2())const highlightedMeshRef = useRef<THREE.Mesh[]>([])// 存储所有创建的边缘线对象const edgeLines = useRef<Map<string, THREE.LineSegments>>(new Map())// 添加边缘高亮效果const addHighlight = (object: THREE.Mesh) => {if (!object.geometry) return// 创建边缘几何体const geometry = new THREE.EdgesGeometry(object.geometry)// 创建边缘线材质const material = new THREE.LineBasicMaterial({color: 0x4c8bf5, // 蓝色边缘linewidth: 2, // 线宽})// 创建边缘线对象const line = new THREE.LineSegments(geometry, material)line.name = 'surroundLine'// 复制原始网格的变换line.position.copy(object.position)line.rotation.copy(object.rotation)line.scale.copy(object.scale)// 设置为模型的子对象,确保跟随模型变换object.add(line)edgeLines.current.set(object.uuid, line)}// 移除边缘高亮效果const removeHighlight = (object: THREE.Mesh) => {const line = edgeLines.current.get(object.uuid)if (line && object.children.includes(line)) {object.remove(line)}edgeLines.current.delete(object.uuid)}// 处理左键点击事件// const handleClick = (event: MouseEvent) => {//   // 仅响应左键点击(排除右键/中键/滚轮)//   if (event.button !== 0) return//   // 计算点击位置的标准化设备坐标//   pointer.current.x = (event.clientX / window.innerWidth) * 2 - 1//   pointer.current.y = -(event.clientY / window.innerHeight) * 2 + 1//   // 执行射线检测,判断点击目标//   detectClickedMesh()// }// 检测点击的Mesh并切换高亮状态// const detectClickedMesh = () => {//   if (!modelRef.current) return//   // 更新射线(从相机到点击位置)//   raycaster.current.setFromCamera(pointer.current, camera)//   // 检测与模型的交点(递归检测所有子Mesh)//   const intersects = raycaster.current.intersectObject(modelRef.current, true)//   if (intersects.length > 0) {//     const clickedObject = intersects[0].object as THREE.Mesh//     // 仅处理标记为可交互的Mesh//     if (//       clickedObject instanceof THREE.Mesh &&//       clickedObject.userData.interactive//     ) {//       // 切换高亮状态:点击已高亮的Mesh则取消,否则高亮新Mesh//       if (highlightedMeshRef.current?.includes(clickedObject)) {//         console.log('取消高亮', clickedObject.name)//         // 移除边框高亮//         removeHighlight(clickedObject)//         const newHighlighted = highlightedMeshRef.current.filter(//           (m) => m.name !== clickedObject.name,//         )//         highlightedMeshRef.current = [...newHighlighted]//       } else {//         console.log('高亮', clickedObject.name)//         // 添加边框高亮//         addHighlight(clickedObject)//         highlightedMeshRef.current = [//           ...highlightedMeshRef.current,//           clickedObject,//         ]//       }//     }//   }// }// 模型加载后初始化useEffect(() => {if (!modelRef.current) returnaddModel()const box = new THREE.Box3().setFromObject(modelRef.current)const center = new THREE.Vector3()box.getCenter(center)const size = new THREE.Vector3()box.getSize(size)// 2. 将模型中心移到世界原点(居中)modelRef.current.position.sub(new THREE.Vector3(center.x, 0, center.z)) // 反向移动模型,使其中心对齐原点const maxDim = Math.max(size.x, size.y, size.z)const fov = 100const cameraZ = Math.abs(maxDim / 2 / Math.tan((Math.PI * fov) / 360))camera.position.set(0, maxDim * 0.3, cameraZ * 1)camera.lookAt(0, 0, 0)// 遍历模型设置通用属性并标记可交互modelRef.current.traverse((child) => {if (child instanceof THREE.Mesh) {child.castShadow = truechild.receiveShadow = truechild.material.transparent = true// 标记为可交互(后续可通过此属性过滤)child.userData.interactive = truechild.material.color.setStyle('#040912')addHighlight(child)// 保存原始材质(用于后续恢复或高亮逻辑)if (!child.userData.baseMaterial) {child.userData.baseMaterial = child.material // 存储原始材质}}})// 绑定点击事件监听// window.addEventListener('click', handleClick)// 组件卸载时清理return () => {// window.removeEventListener('click', handleClick)// 移除所有高亮边缘线highlightedMeshRef.current.forEach((mesh) => {removeHighlight(mesh)})edgeLines.current.clear()highlightedMeshRef.current = []}}, [modelRef.current])// 添加模型到管理器const addModel = () => {if (modelRef.current) {helper.addModel({id: '模型1',name: '模型1',url: url,model: modelRef.current,})}}return (<><primitive object={scene} ref={modelRef} />{/* 扩散光圈:位于模型中心,与模型平面平行 */}{/* 内层光圈:暖红色系,扩散范围最小,亮度最高 */}<DiffuseAperturecolor="#ff6b3b" // 内层暖红(鲜艳)initialRadius={0.1}maxRadius={15} // 最小扩散范围expandSpeed={2} // 中等扩散速度fadeSpeed={0.6} // 较慢淡出(停留更久)height={0.08}/>{/* 中层光圈:橙黄色系,衔接内外层 */}<DiffuseAperturecolor="#ffc154" // 中层橙黄(过渡色)initialRadius={0.2}maxRadius={20} // 中等扩散范围expandSpeed={2.5} // 稍快于内层fadeSpeed={0.7} // 中等淡出速度height={0.06}/>{/* 外层光圈:蓝紫色系,扩散范围最大,亮度最低 */}<DiffuseAperturecolor="#609bdf" // 外层浅蓝(冷色)initialRadius={0.3}maxRadius={25} // 最大扩散范围expandSpeed={3} // 最快扩散速度fadeSpeed={0.8} // 最快淡出(快速消失)height={0.04}/></>)
}

 总结:技术点与应用场景

这个扩散光圈特效的核心是 “动态变化”—— 通过 React Three Fiber 的 useFrame 实现每一帧的更新,用 Three.js 的圆柱几何体和材质属性控制视觉表现。它的优势在于:

  1. 性能友好:只用一个简单的圆柱几何体,避免复杂计算
  2. 易于控制:通过参数可轻松调整大小、速度、颜色
  3. 场景适配:旋转和位置调整可适配任何 3D 模型

在 CityModel 中,这个特效可用于:

  • 城市加载完成的 “开场动画”
  • 用户点击城市中心的 “交互反馈”
  • 模拟信号覆盖、能量扩散等业务场景

通过这个小功能,我们可以感受到 3D 特效的魅力 —— 看似复杂的视觉效果,往往是由简单的几何变换和动态更新组合而成。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88635.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88635.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【牛客刷题】COUNT数字计数

文章目录 一、题目介绍二、题解思路三、算法实现四、复杂度分析五 、关键步骤解析5.1 数字分解5.2 三种情况处理5.2.1 情况1: d < c u r d < cur d<cur(完整周期)5.2.2 情况2: d = c u r d = cur d=cur(混合周期)5.2.3 情况3: d > c u r d > cur d>cu…

AGV穿梭不“迷路”CCLinkIE转Modbus TCP的衔接技巧

在AGV控制系统集成中&#xff0c;工程师常面临一个现实难题&#xff1a;如何让CCLinkIE总线与Modbus TCP设备实现高效通信&#xff1f;这种跨协议的连接需求&#xff0c;往往需要耗费大量时间调试。本文将通过实际案例解析&#xff0c;为制造行业工程师提供可复用的解决方案。【…

【代码随想录】刷题笔记——哈希表篇

目录 242. 有效的字母异位词 349. 两个数组的交集 202. 快乐数 1. 两数之和 454. 四数相加 II 383. 赎金信 15. 三数之和 18. 四数之和 242. 有效的字母异位词 思路 代码 class Solution {public boolean isAnagram(String s, String t) {if (s.length() ! t.length()…

Python爬虫实战:研究messytables库相关技术

1. 引言 在当今数字化时代,互联网上存在着大量有价值的数据。然而,这些数据通常以不规则的格式存在,尤其是表格数据,可能包含复杂的表头、合并单元格、不规则布局等问题。传统的数据处理工具往往难以应对这些挑战。 网络爬虫技术可以帮助我们从网页上自动提取数据,而 mes…

Vue3的组件通信方式

通信方式适用层级数据流向复杂度Props/Emits父子组件单向/双向★☆☆v-model父子组件双向★☆☆Provide/Inject跨层级组件自上而下★★☆事件总线任意组件任意方向★★★Pinia/Vuex全局状态任意方向★★☆Refs模板引用父子组件父→子★☆☆作用域插槽父子组件子→父★★☆Web W…

创客匠人:大健康创始人IP如何用“社会责任”构建品牌护城河

一、商业与责任的失衡困局部分大健康IP将利润置于首位&#xff0c;甚至牺牲用户利益&#xff0c;导致品牌形象脆弱。某保健品公司因夸大宣传被曝光后&#xff0c;尽管销量曾达千万&#xff0c;却因缺乏社会认同&#xff0c;一夜之间崩塌&#xff0c;证明没有社会责任支撑的商业…

AI:机器人未来的形态是什么?

机器人未来的形态将受到技术进步、应用场景需求和社会接受度的综合影响&#xff0c;以下是对未来机器人形态的预测&#xff0c;涵盖技术趋势、设计方向和应用场景&#xff1a; 1. 形态多样化与通用化 人形机器人&#xff08;Humanoid Robots&#xff09;&#xff1a; 趋势&…

创建 UIKit 项目教程

一、打开 XCode&#xff0c;选择 iOS 下的 App&#xff0c;然后点 Next二、Interface 选择 Storyboard&#xff0c;然后点 Next三、删掉 Main.storyboard四、删掉 SceneDelegate.swift五、AppDelegate.swift 只保留第一个函数六、在 AppDelegate.swift 文件里的 application 函…

防爬虫君子协定 Robots.txt 文件

1.什么是robots.txt ? robots.txt是一个位于网站根目录的文本文件,用于指导搜索引擎爬虫如何访问和抓取网站内容。它遵循特定的语法规则,是网站与爬虫通信的重要工具。当搜索引擎访问一个网站时,它首先会检查该网站的根域下是否有一个叫做robots.txt的纯文本文件。Robots.…

浅谈 Python 中的 yield——生成器对象与函数调用的区别

我们来看这么一个例子&#xff1a; def greeter():name yield "你是谁&#xff1f;"yield f"你好&#xff0c;{name}"g greeter() print(next(g)) # → "你是谁&#xff1f;" print(g.send("张三")) # → "你好&#xf…

云端docker小知识

1、docker的三个关键概念image、container、dockerfile2、docker的container3、dockerfile4、docker制作image5、linux&#xff08;ubuntu&#xff09;安装docker&#xff08;步骤1和4&#xff09;6、docker基本命令docker images 查看全部镜像docker rmi -f 1e5f3c5b981a 删除…

【Elasticsearch】昂贵算法与廉价算法

在 Elasticsearch 里&#xff0c;“昂贵”并不单指“CPU 时间”&#xff0c;而是综合了 **CPU、内存、磁盘 I/O、网络传输** 以及 **实现复杂度** 的代价。下面把常见“昂贵算法”拆开说&#xff1a;1. **高计算密度的文本算法** • **match_phrase slop**&#xff08;带跨距…

深度学习-多分类

​开头摘要​​&#xff1a; 本文将深入探讨如何使用PyTorch实现基于Softmax回归的MNIST手写数字识别系统。从多分类问题的核心概念出发&#xff0c;详细解析​​One-Hot编码​​技术如何将类别标签向量化&#xff0c;剖析​​交叉熵损失函数​​的数学原理及其在训练中的优化机…

JVM 类加载过程

一、加载&#xff08;Loading&#xff09;目标&#xff1a;把字节码文件&#xff08;.class&#xff09;“读入 JVM”&#xff0c;生成类的 “半成品”&#xff08;Class 对象&#xff09;。Bootstrap ClassLoader&#xff08;启动类加载器&#xff09;&#xff1a;负责加载 JV…

通俗范畴论13 鸡与蛋的故事番外篇

通俗范畴论13 鸡与蛋的故事番外篇 在上一篇中,我们得到了鸡与蛋的Set局部小范畴如下: 鸡与蛋 SetSetSet 局部小范畴 如上图所示,每个鸡来自于一个蛋,每个蛋来自于一只鸡,如此循环,以至于无穷… 是的,假设鸡与蛋两个对象代表的集合,都是无穷集合,这个系统就没有问题…

记录跟随recyclerview滑动的指示器

老早之前做的一个功能&#xff0c;横向recyclerview滑动时&#xff0c;底部做跟随滑动指示器。今天代码不用了&#xff0c;记录下代码。<LinearLayoutandroid:layout_width"match_parent"android:layout_height"wrap_content"android:layout_marginTop&…

快速过一遍Python基础语法

前言 本文章是深度学习的前导课&#xff0c;对有编程基础的小伙伴更加的友好&#xff08;C、C&#xff09;&#xff0c;如果完全没有学过任何一门编程语言也没有关系&#xff0c;本文章不会涉及到晦涩难懂的原理&#xff0c;只是简单的带大家过一遍Python的基础语法。 下面的操…

[爬虫实战] 多进程/多线程/协程-异步爬取豆瓣Top250

相关爬虫知识点&#xff1a;[爬虫知识] 深入理解多进程/多线程/协程的异步逻辑 相关爬虫专栏&#xff1a;JS逆向爬虫实战 爬虫知识点合集 爬虫实战案例 逆向知识点合集 前言&#xff1a; 在之前文章中&#xff0c;我们深入探讨了多进程、多线程和协程这三大异步技术的工作…

Git系列--1.初始Git

一、背景 目录 一、背景 二、认识 三、如何在Linux上安装Git 3.1检测git是否存在和版本 3.2安装和卸载git 3.2.1Centos 3.2.2Ubuntu 四、基本操作 4.1创建本地仓库 4.2必须的配置项 4.3宏观认识基本分区 我们会根据需求不断更改我们的文件内容&#xff0c;但有时我们会…

QWidget的属性

QWidget的属性 windowOpacityAPI说明windowOpacity()获取不透明数值&#xff0c;返回float&#xff0c;取值为0.0到1.0&#xff0c;其中0.0为全透明&#xff0c;1.0为完全不透明setWindowOpacity()设置控件的不透明数值注意点&#xff1a;窗口不透明度的变化并非精确的&#xf…