如何用Three.js + Blender打造一个web 3D展览馆

news/2024/5/10 20:02:51/文章来源:https://blog.csdn.net/vivo_tech/article/details/131717174

作者:vivo 互联网前端团队- Wei Xing 

运营活动新玩法层出不穷,web 3D炙手可热,本文将一步步带大家了解如何利用Three.js和Blender来打造一个沉浸式web 3D展览馆。

一、前言

3D展览馆是什么,先来预览下效果:

图片

看起来像个3D冒险类手游,用户可以操纵屏幕中央的虚拟摇杆,以第一人称视角在房间内自由移动、看展览。

1.1 为什么做3D展览馆

首先介绍一个背景,我们的工作内容是做游戏中心的用户运营活动,会做些好玩的活动让用户参与,并get一些福利。

当时的活动背景是我司一年一度的vivo游戏节,并且元宇宙是大热词。所以做它的原因有几个:

  • vivo游戏节主题

  • 契合元宇宙热点

  • 新玩法、新体验

1.2 技术选型

用到的组合方案:Three.js + Blender

  • why Three.js

开源的3D框架有很多,但最常用的有两种:Three.js、Babylon.js,我们只需要从中二选一。分析后发现两者各有优势:

图片

考虑到3D展览馆的几个基本特性:

  1. 简单的小型3D场景,没有复杂的交互(对镜头的要求不高)

  2. 投放在移动设备,需要尽可能小的包体,以提升性能

  3. 工期短,需要快速上手及更多的案例参考

Three.js包体更小、有更多参考案例、上手更快,所以虽然Babylon.js有它的优势,但Three.js更适合这个项目。

  • why Blender

Blender是一款轻量的开源3D建模软件,有很多好用的免费插件,而且Blender能导出GLTF / GLB模型(后面会对GLTF / GLB模型做简介),匹配Three.js的使用方式,整体更简单好用一些。

所以,就是它了。

二、实践部分

2.1 了解GLTF / GLB模型

在进入开发之前,先简单了解Blender和GLTF / GLB模型。

  • 简单了解 Blender

首先,Blender大概长这样,图中是设计师交付的3D展览馆稿子。简单理解为,左侧是模型的层次结构,中间是模型的预览效果,右侧是模型的属性面板。

一般来说,作为开发者我们不需要掌握太多Blender相关知识,只需知道如何看懂模型结构、导出GLTF / GLB模型以及烘焙的基本原理即可。

图片

  • GLTF / GLB模型

GLTF(Graphics Language Transmission Format)是一种标准的3D模型文件格式,它以JSON的形式存储3D模型信息,例如模型的层次结构、材质、动画、纹理等。

模型中依赖的静态资源,比如图片,可以通过外部URI的方式来引入,也可以转成base64直接插入在GLTF文件中。

它包含两种形式的后缀,分别是.gltf(JSON/ASCII).glb(Binary)。.gltf是以JSON的形式存储信息。.glb则是.gltf的扩展格式,它以二进制的形式存储信息,因此导出的模型体积也更小一些。如果我们不需要通过JSON对.gltf模型进行直接修改,建议使用.glb模型,它更小、加载更快。

  • Blender导出GLTF / GLB模型

在blender中,可以直接将模型导出为GLTF / GLB格式,三种选项的差别不再赘述,我们先简单选择最高效的.glb格式。

图片

有了模型之后,我们可以开始通过Three.js创建场景,并导入这个模型了。

2.2 Three.js 加载模型

为了防止篇幅过长,这里假设大家已经掌握了Three.js的一些基本语法。文章重点放在如何加载模型,并一步步进行调优和实现最终的3D展览馆效果。

怎么加载一个模型?

(1)创建一个空场景

首先创建一个空场景scene,后续所有的模型或材质都会被添加到这个场景中。

import * as THREE from 'three'// 1. 创建场景
const scene = new THREE.Scene(); // 2. 创建镜头
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
// 3. 创建Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

(2)导入GLTF / GLB模型

通过GLTFLoader导入.glb模型,并添加到场景中。

import GLTFLoader from 'GLTFLoader'
const loader = new GLTFLoader()
loader.load('path/to/gallery.glb',gltf => {scene.add(gltf.scene) // 添加到场景中} 
}

(3)开始渲染

通过requestAnimationFrame来调用renderer.render方法,开始实时渲染场景。

function animate() {requestAnimationFrame( animate );renderer.render( scene, camera );
}
animate();

ok,这样我们就完成了3D模型的导入,但是发现整个场景一片漆黑。

图片

试试加个环境光。

const ambientLight = new THREE.AmbientLight(0xffffff, 1)scene.add(ambientLight)

图片

ok,亮起来了,但是效果依然很差,很劣质。

原因是模型中的材质效果、光源、阴影、环境纹理,这些全都丢失了,所以当我们导入模型时,看到的就是一堆简陋的纯色形状。

所以我们要一步步将这些丢失东西找回,还原设计稿。

2.3 还原设计稿

接下来一步步还原设计稿。

(1)加上光源

查看Blender模型,看到设计稿中添加了一堆点光源、平行光源。

图片

点光源可以理解为房间中的灯泡,光线强弱随着距离衰减;

平行光源可以理解为太阳的直射光,它和点光源不同,光线强弱不随着距离衰减。

于是我们也增加一些光源:

// 一些灯光选项
// 如果是平行光则没有distance、decay选项
const lightOptions = [{type: 'point', // 灯光类型:1. point点光源、2. directional平行光源color: 0xfff0bf, // 灯光颜色intensity: 0.4, // 灯光强度distance: 30,    // 光照距离decay: 2,    // 衰减速度position: { // 光源位置x: 2,y: 6,z: 0}},...]function createLights() {pointLightOptions.forEach(option => {const light = option.type === 'point' ?new THREE.PointLight(option.color, option.intensity, option.distance, option.decay) :new THREE.DirectionalLight(option.color, option.intensity)const position = option.positionlight.position.set(position.x, position.y, position.z)scene.add(light)})
}createLights()

可以看到场景比之前好了一些,有了光源后,模型变得立体和真实了,多了一些反色的光泽。

图片

图片

但是我们注意到,画面中的logo、长椅的两侧都是黑色的,并且旁边的球体、椅子等都显得不够真实。

所以,我们需要进行下一步调整:调整模型材质、增加环境纹理。

(2)调整模型材质,增加环境纹理

先简单了解一下材质和环境纹理。

  • 材质(material)

材质就像物体的皮肤,我们可以调整皮肤的光泽、金属度、粗糙度、透明与否等属性,让物体有不同的视觉效果。

一般从blender导出的模型中,已经包含了一些材质属性,但是Three.js中的材质属性和Blender中的属性并非完全的映射关系,模型在导入到Three.js后,效果和设计稿会有差异。这时候我们需要手动调整材质的属性,来达到和设计稿近似的效果。

  • 环境纹理(environment map)

环境纹理就是让模型映射周围的环境,让场景或物体更真实。例如我们要渲染一个立方体,把立方体放进一个屋子里,这个屋子的环境就会影响立方体的渲染效果。

比如镜面的物体被贴上环境纹理后,就可以实时反射周围的环境镜像,看起来很real。

设计稿中也是将一个大厅作为了环境纹理,让场景更真实。

图片

环境纹理分为:球形纹理和立方体形纹理。两者都可以,这里我们采用一张大厅的球形纹理作为环境贴图。

图片

以画面中的vivo游戏节logo为例,我们通过调整它的材质和环境纹理,让它变得更真实。

  1. 根据在blender中的命名,找到logo模型

  2. 调整logo的表面粗糙度和金属度

  3. 加载并设置环境纹理贴图

图片

const loader = new GLTFLoader()
loader.load('path/to/gallery.glb',gltf => {// 1. 根据Blender中物体的名字,找到logo模型gltf.scene.traverse(child => {if (isLogo(child)) {initLogo(child)   // 2. 调整材质setEnvMap(child)  // 3. 设置环境纹理}})scene.add(gltf.scene)} 
}// 判断是否为Logo
const isLogo = object.name === 'logo'function initLogo(object) {object.material.roughness = 0   // 调整表面粗糙度object.material.metalness = 1   // 调整金属度
}
// 加载环境纹理let envMap
const envmaploader = new THREE.PMREMGenerator(renderer)const setEnvMap = (object) => {if(envMap) {object.material.envMap = envMap.texture} else {textureLoader.load('path/to/envMap.jpg',texture => {texture.encoding = THREE.sRGBEncodingenvMap = envmaploader.fromCubemap(texture)object.material.envMap = envMap.texture})}
}

经过上面的处理后,可以看到原先黑色的logo有了金属光泽,并且会反射周围的环境纹理。

其它物体经过类似的处理后,也变得更真实一些。

图片

图片

图片

图片

现在整个场景更接近了设计稿一些,但场景中少了阴影,显得很干瘪。

加上阴影。

(3)增加阴影

增加阴影分四步:

  1. 对renderer开启阴影支持:renderer.shadowMap.enabled = true

  2. 对光源设置:castShadow = true

  3. 对需要投影的物体设置:castShadow = true

  4. 对需要被投影的平面或物体(比如地板)设置:receiveShadow = true

// 1. renderer
const renderer = new THREE.WebGLRenderer()
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;// 2. light
const light = new THREE.DirectionalLight()
light.castShadow = true;// 3. object
gltf.scene.traverse(function (child) {if (child.isMesh) {child.castShadow = true;}
});
// 4. floor
floor.receiveShadow = true

图片

添加阴影后,有质的提升,发现整个场景立体了很多,此时还原度已经很高。

如果不考虑性能损耗,这个场景的样式已经可以投入使用了。(后续会提到性能优化)

小结一下,刚刚做的几件事:

  1. 添加光源

  2. 调整模型材质、增加环境纹理

  3. 增加阴影

现在3D展览馆场景已经还原的差不多了,接下来要构造一个虚拟移动摇杆,控制第一人称镜头的移动和转向,实现沉浸式逛展的效果。

2.4 虚拟移动摇杆

要实现通过虚拟移动摇杆控制镜头的移动和转向,我们需要三个东西:

  • 一个移动摇杆(handler)

  • 一个长方体(player):用于承载第一人称视角

  • 一个镜头(camera):之前已经创建过了

有人会问为什么需要一个player,通过摇杆直接控制镜头不就行了吗?其实player的作用是用于做碰撞检测,当player遇到凳子、墙壁等障碍物时,需要停止镜头移动。直接控制镜头,是无法做碰撞检测的。

所以,实际上镜头移动的逻辑是:

用户操纵摇杆 → 更新player位置和朝向 →从而同步更新camera位置和朝向

(1)创建移动摇杆

移动摇杆的实现原理很简单,这里仅做简述。

核心在于创建一个圆盘,监听触摸手势,并根据手势的方向来实时更新move参数,控制镜头的移动和转向。

const speed = 8 // 移动速度
const turnSpeed = 3  // 转向速度
// move option,用于调整第一人称镜头的移动和转向
const move = {turn: 0,  // 旋转角度forward: 0     // 前进距离
}// 创建一个handler,并监听手势,调整move option
const handler = new Handler()
handler.onTouchMove = () => { // update move option }

(2)创建player

首先创建一个player对象,它是一个1.2 * 2 * 1的透明长方体。

function createPlayer() {const box = new THREE.BoxGeometry(1.2, 2, 1)const mat = new THREE.MeshBasicMaterial({color: 0x000000,wireframe: true})const mesh = new THREE.Mesh(box, mat)box.translate(0, 1, 0)return mesh
}const player = createPlayer() // 创建player
player.position.set(4.5, 2, 12)     // 设置player的初始位置

(3)updatePlayer & updateCamera

每次渲染(render)时,更新player的位置和朝向,并同步更新镜头的位置和朝向。

const clock = THREE.clock()function render() {const dt = clock.delta()   // 获取每帧之间的时间间隔,根据时间间隔长短来更新player和camera的移动距离和转向的多少updatePlayer(dt)updateCamera(dt)renderer.render(scene, camera)window.requestAnimationFrame(render)
}// 更新player的位置和朝向function updatePlayer(dt) {const pos = player.position.clone()pos.y -= 1.5 // 降低高度,后续用于计算碰撞检测const dir = new THREE.Vector3()player.getWorldDirection(dir)dir.negate()if (move.forward < 0) dir.negate()// 调整镜头前进 or 后退if (move.forward !== 0) {player.translateZ(move.forward > 0 ? -dt * speed : dt * speed * 0.5)}// 调整镜头朝向if (move.turn !== 0) {player.rotateY(move.turn * 1.2 * dt)}
}// 根据player的位置和朝向,同步更新camera的位置和朝向function updateCamera(dt) {camera.position.lerp(activeCamera.getWorldPosition(new THREE.Vector3()), 0.08)const pos = player.position.clone()pos.y += 2.5camera.lookAt(pos)
}

注意:render方法中使用clock.delta()来计算每次渲染之间的时间间隔,并使用这个时间间隔来更新player和camera。因为在理想的60帧率情况下,两帧时间间隔为16.67ms,但实际上该数值会有波动,因此我们要根据实际的渲染时间间隔来更新player和camera,让镜头的移动和转向幅度更自然一些。

完成上述步骤后,我们就可以通过控制虚拟移动摇杆,来让镜头移动和转向了。

接下来加入碰撞检测,对镜头移动加点限制。

2.5 碰撞检测

碰撞检测的步骤也很简单:

  • 收集障碍物(colliders)

  • 检测碰撞(基于THREE.Raycaster)

(1)收集障碍物

模型加载完成后,遍历所有的child,如果child是一个物体(mesh),则把它加入到障碍物队列(colliders)中。

const colliders = []loader.load('path/to/gallery.glb',gltf => {gltf.scene.traverse(child => {// 收集障碍物if(isMesh(child)) {colliders.push(child) }})} 
})

(2)检测碰撞

调整刚刚的updatePlayer方法,在其中插入检测碰撞的逻辑。

碰撞检测逻辑基于THREE.Raycaster来实现,racaster可以理解为一个射线,当射线穿过了某个物体,我们就认为射线和物体相交了。

我们让射线的方向和player的朝向保持一致,并且在移动过程中不断判断射线前方/后面是否有相交的物体,如果有相交的物体,且和射线顶点距离distance < 2.5则认为遇到了障碍物,不能再继续前进。

function updatePlayer(dt) {const pos = player.position.clone()pos.y -= 1.5 // 降低高度,用于计算collisionconst dir = new THREE.Vector3()// 获取当前player的朝向player.getWorldDirection(dir)dir.negate()// 如果是向后退,需要对朝向取反if (move.forward < 0) dir.negate()// 利用Raycaster判断player是否和colliders有碰撞行为const raycaster = new THREE.Raycaster(pos, dir)let blocked = falseif (colliders.length > 0) {const intersect = raycaster.intersectObjects(colliders)if (intersect.length > 0) {// 如果相交距离<2.5,表示前方或后面有障碍物if (intersect[0].distance < 2.5) {blocked = true}}}// 如果遇到障碍物,则停滞移动if (!blocked) {// 调整镜头前进 or 后退if (move.forward !== 0) {player.translateZ(move.forward > 0 ? -dt * speed : dt * speed * 0.5)}}// 调整镜头朝向if (move.turn !== 0) {player.rotateY(move.turn * 1.2 * dt)}
}

这样镜头的移动和碰撞检测就完成了。

当我们移动到椅子、墙壁等障碍物附近时,镜头会停止移动。镜头的移动范围也被我们限制在房间里,不会穿到房间外部。

图片

三、性能调优

3.1 纹理烘培

3D展览馆的基本功能已经完成了,但还没有做任何的性能调优。当我们把项目运行在手机上,会发现设备发热发烫,帧率很低,低端机型甚至无法运行。

经过分析,实时的光影渲染是罪魁祸首。

页面中有10+个光源,每个光源都在实时投射阴影(尤其是点光源十分消耗资源,引起卡顿)。但实际,场景中的光源和物体位置都没有发生改变,这意味着我们不需要计算实时阴影,只需要固定的阴影。

这点可以通过纹理烘焙来实现。并且在移动端,经过纹理烘焙的光影效果实际上要优于设备计算的实时光影效果。

  • 纹理烘焙(Texture Baking)

纹理烘焙,是指通过将场景效果预渲染到指定纹理上,生成一个模型贴图。在Blender中,我们可以选中任意对象进行烘焙。

图片

以3D展览馆的地板为例,我们可以通过纹理烘焙,将光影效果直接渲染到贴图上。

左图是原本的棋盘格纹理,右图是结合了光影效果的烘焙贴图。烘焙完成后,地板上的光影效果就被固定下来了,我们也不需要再做实时的光影渲染。

图片

图片

用同样的方式,将地板、墙壁、天花板等物体,一一进行烘焙处理,导出一个新的模型。由于光影效果已经被渲染到贴图上,我们可以将大部分光源去掉,只保留2-3个必要的点、平行光源和全局光。再次运行后,发现卡顿、发烫的问题已经不再明显。并且效果其实比实时渲染更精细一些。

图片

图片

这里没有对烘焙做过多介绍,要生成精致的烘焙结果还需要依赖对UV Map、烘焙参数的了解,虽然这些偏向于设计同学的工作,一般由他们来输出烘焙纹理。但是作为开发者,了解了这些后才能和UI更好地沟通和配合。

3.2 优化模型大小

模型大小约为23M,首次加载模型需要9s左右。(尤其是在做完纹理烘焙后,由于贴图变得复杂,模型更大了)

以下是几个优化模型大小的建议:

  1. 优先使用.glb而非.gltf格式。.glb是二进制格式,它比.gltf的JSON格式小25% - 30%左右。

  2. 将纹理(Texture)和模型分离,并行加载。23M的模型中,其实只有2.3M为模型大小,其余都为纹理贴图。将模型和纹理分开后,可以极大减少模型的加载速度。

  3. 使用Draco、gltfpack等工具或一些online compressor来压缩模型(Blender在导出gltf模型时,就带有基于Draco的压缩选项)。本项目通过该步骤压缩了50%的模型大小:3M → 1.2M。

  4. 压缩纹理(Texture)。本项目用到了5张的Texture,压缩后:18M→ 2M。

经过优化,初始模型大小由23M缩小为1.2M,首次加载时间由9s缩短到3s以内。

(左图为优化前,右图为优化后)

图片

图片

四、总结

现在,我们基本完成了整个3D展览馆的开发。虽然有一些细节没有在文中涉及到,但开发过程大致如此。

(1)了解Blender、GLTF / GLB模型

(2)js导入GLTF / GLB模型

(3)还原设计稿

  • 添加光源

  • 调整模型材质、增加环境纹理

  • 增加阴影

(4)实现虚拟移动摇杆,控制镜头移动

(5)增加碰撞检测

(6)性能调优:

  • 纹理烘培:通过纹理烘焙降低实时光影的性能损耗。

  • 优化包体大小:

- 优先使用.glb而非.gltf格式

- 纹理和模型分离

- 压缩模型

- 压缩纹理

五、其他

一些建议:

  • 设计师在Blender中命名物体、材质时要规范化,避免出现奇怪或没有标识意义的命名,因为在开发过程中会使用到,容易混淆。

  • 设计师在在Blender中复用材质要谨慎,避免开发在调整某个材质时,影响到其它使用到相同材质的物体(潜在bug)。

  • 模型加载缓慢时,可以增加loading进度条,缓解等待焦虑。Three.js loader支持加载进度查询。

  • Three.js在不同版本之间,接口频繁变更,在使用时注意版本差异,google问题时也要注意接口兼容性。

  • Three.js实现物体发光效果较繁琐,且消耗性能,设计时可尽量避免使用。

  • Three.js的镜头移动不够丝滑,注重镜头切换流畅性的项目,可以尝试用Babylon.js。

  • 部分浏览器不支持videoTexture(在模型中播放视频),谨慎设计该类型功能,或做好兼容处理。

参考:

  • Threejs+Blender打造3D全景VR画展「1」 – 码语派教室

  • three js blender animation - Google Search

  • Loading Animated Characters in React Three Fiber

  • 浅谈three.js中的needsUpdate - pissang - 博客园

  • Directional Light Shadow - Three.js Tutorials

  • Shadows not working

  • 自适应Shadow Bias算法

  • GLB Modify Material and add emission

  • How can I improve the performance of a three.js script?

  • HTML5 Audio events not triggering on Chrome

  • http://jeromeetienne.github.io/threex.videotexture/examples/videotexture.html

  • 部分代码参考自:GitHub - mayupi/3dvr-gallery: 3dvr gallery with threejs and blender

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

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

相关文章

Linux离线环境Jenkins部署SpringBoot

Jenkins服务器 把Jar包上传到Linux服务器的/jenkins/目录下 Dashboard----》新建任务----》构建一个自由风格的软件项目----》test 修改jenkins工作空间 新建构建前执行命令stop.sh&#xff0c;停止SpringBoot并备份 &#xff08;这里是目标服务器&#xff0c;即部署项目的…

激斗云计算:互联网大厂打响新一轮排位战

大模型如同一辆时代列车&#xff0c;所有科技大厂都想上车。 自去年底ChatGPT一炮而红&#xff0c;国内外数十家科技大厂、创业公司、机构相继下场&#xff0c;一时间掀起大模型的热浪。 《中国人工智能大模型地图研究报告》显示&#xff0c;截至今年5月28日&#xff0c;中国…

第八章:SegNet——一个用于强大的语义像素级标注的深度卷积编码-解码架构

0.摘要 我们提出了一种新颖的深度架构SegNet&#xff0c;用于语义像素级图像标注。SegNet具有一些吸引人的特性&#xff1a; (i)它只需要对完全学习的函数进行前向评估&#xff0c;就可以获得平滑的标签预测&#xff1b; (ii)随着深度增加&#xff0c;像素标注考虑了更大的上下…

SpringBoot+actuator和admin-UI实现监控中心

使用SpringBoot很久了&#xff0c;但是很少使用到SpringBoot的查看和监控&#xff0c;将来八成也不会用到&#xff0c;万一有机会用到呢&#xff1f;所以记录一下以前学习SpringBootactuator和adminUI实现监控中心的方式 Springboot的版本2.0.x <parent><groupId>…

keepalived安装配置详解

文章目录 高可用介绍keepalived安装、使用vip漂移抓包脑裂脑裂有没有危害&#xff1f;如果有危害对业务有什么影响&#xff1f; keepalived架构双vip架构 Healthcheck实现 notifyVRRP选举格式 高可用 介绍 高可用性&#xff08;High Availability&#xff09;是指系统或服务能…

Word2Vec实现文本识别分类

深度学习训练营之使用Word2Vec实现文本识别分类 原文链接环境介绍前言前置工作设置GPU数据查看构建数据迭代器 Word2Vec的调用生成数据批次和迭代器模型训练初始化拆分数据集并进行训练 预测 原文链接 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&…

pycharm新建分支并提送至GitHub

文章目录 前言pycharm创建本地分支Push至远程分支 前言 当我们写的项目代码越来越多时&#xff0c;一个master分支无法满足需求了&#xff0c;这个时候就需要创建分支来管理代码。 创建分支可以快速的回滚到某个节点的版本&#xff0c;也可以多个开发者同时开发一个项目&#…

剑指oferr68-II.二叉树的最近公共祖先

为什么这道题的难度是easy&#xff0c;我感觉挺难的啊&#xff0c;我想了挺久没有一点思路就直接看题解了。题解有两种解法&#xff0c;先看第一种存储父节点 class Solution {Map<Integer,TreeNode> parent new HashMap<Integer,TreeNode>();Set<Integer>…

windows nodejs 版本切换

一、按健winR弹出窗口&#xff0c;键盘输入cmd,然后敲回车。然后进入命令控制行窗口&#xff0c;并输入where node查看之前本地安装的node的路径。 二、找到上面找到的路径&#xff0c;将node.exe所在的父目录里面的所有东西都删除。 三、从官网下载安装包 https://github.com/…

Raft算法之日志复制

Raft算法之日志复制 一、日志复制大致流程 在Leader选举过程中&#xff0c;集群最终会选举出一个Leader节点&#xff0c;而集群中剩余的其他节点将会成为Follower节点。Leader节点除了向Follower节点发送心跳消息&#xff0c;还会处理客户端的请求&#xff0c;并将客户端的更…

AP5193 DC-DC宽电压LED降压恒流驱动器 LED电源驱动IC

产品 AP5193是一款PWM工作模式、外围简单、内置功率MOS管&#xff0c;适用于4.5-100V输入的高精度降压LED恒流驱动芯片。电流2.5A。AP5193可实现线性调光和PWM调光&#xff0c;线性调光脚有效电压范围0.55-2.6V.AP5193 工作频率可以通过RT 外部电阻编程来设定&#xff0c;同时…

linux下一个iic驱动(按键+点灯)-互斥

一、前提&#xff1a; 硬件部分&#xff1a; 1. rk3399开发板&#xff0c;其中的某一路iic&#xff0c;这个作为总线的主控制器 2. gd32单片机&#xff0c;其中的某一路iic&#xff0c;从设备。主要是按键上报和灯的亮灭控制。&#xff08;按键大约30个&#xff0c;灯在键的…

day34-servlet 分页

0目录 servlet 1.分页 分页逻辑1&#xff1a;数据库中20条记录&#xff0c;要求每页5条数据&#xff0c;则一共有4页 分页逻辑2&#xff1a;数据库中21条记录&#xff0c;要求每页5条数据&#xff0c;则一共有5页 分页逻辑3&#xff1a;数据库中19条记录&#xff0c;要求每页…

数字 IC 设计职位经典笔/面试题(二)

共100道经典笔试、面试题目&#xff08;文末可全领&#xff09; FPGA 中可以综合实现为 RAM/ROM/CAM 的三种资源及其注意事项&#xff1f; 三种资源&#xff1a;BLOCK RAM&#xff0c;触发器&#xff08;FF&#xff09;&#xff0c;查找表&#xff08;LUT&#xff09;&#xf…

【算法基础】2.1栈和队列(单调栈和单调队列)

文章目录 例题3302. 表达式求值&#xff08;栈的应用&#xff09;&#x1f62d;&#x1f62d;&#x1f62d;&#x1f62d;&#x1f62d;830. 单调栈知识点解法 154. 滑动窗口 &#xff08;单调队列&#xff09;知识点解法 相关链接 & 相关题目 例题 3302. 表达式求值&…

基于weka手工实现多层感知机(BPNet)

一、BP网络 1.1 单层感知机 单层感知机&#xff0c;就是只有一层神经元&#xff0c;它的模型结构如下1&#xff1a; 对于权重 w w w的更新&#xff0c;我们采用如下公式&#xff1a; w i w i Δ w i Δ w i η ( y − y ^ ) x i (1) w_iw_i\Delta w_i \\ \Delta w_i\eta(y…

RabbitMQ 同样的操作一次成功一次失败

RabbitMQ 是一个功能强大的消息队列系统&#xff0c;广泛应用于分布式系统中。然而&#xff0c;我遇到这样的情况&#xff1a;执行同样的操作&#xff0c;一次成功&#xff0c;一次失败。在本篇博文中&#xff0c;我将探讨这个问题的原因&#xff0c;并提供解决方法。 我是在表…

创作一周年纪念日【道阻且长,行则将至】

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; 技术之外的往事 &#x1f383;所处时段&#xff1a; 大学生涯[1/2] 文章目录 一、起点一切皆有定数 二、成果尽心、尽力 三、相遇孤举者难起&#xff0c;众行者易趋 四、未来长风破浪会有时&#xff0c;直挂云…

markdown2html 转化流程

定义一个extensions function markedMention() {return {extensions: [{name: mention,level: inline,start(src) {// console.log("markedMention start....", src);return src.indexOf(#)},tokenizer(src, tokens) {const rule /^(#[a-zA-Z0-9])\s?/const match…

JMeter websocket接口测试

前言 在一个网站中&#xff0c;很多数据需要即时更新&#xff0c;比如期货交易类的用户资产。在以前&#xff0c;这种功能的实现一般使用http轮询&#xff0c;即客户端用定时任务每隔一段时间向服务器发送查询请求来获取最新值。这种方式的弊端显而易见&#xff1a; 有可能造…