Fork me on GitHub
艺术码畜的生活瞬间

playcanvas初识

playcanvas(基于WebGL的游戏引擎)

PlayCanvas 是一款使用 HTML5 和 WebGL 技术运行游戏以及其他 3D 内容的开源游戏引擎,PlayCanvas 以其独特的性能实现了在任何手机移动端和桌面浏览器端均可以流畅运行。

PlayCanvas 引擎是一款可以基于浏览器的用于制作游戏以及 3D 可视化的开源引擎。除此之外,还开发了PlayCanvas 开发平台, 为用户提供了可视化编辑器,资源管理,代码编辑,代码托管以及发布等服务。

目前中文的教程很少,所以英语好的同学可以去看一下国外的那些教程,但是还是有中文文档的。

可视化平台操作上倒是没什么难度,就是和正常的建模操作大同小异,一个可视化的一个操作平台,可以进一步的提高我们工作的效率吧。

一、注册账户

注册账户的方式很简单,根据提示去注册就可以了

二、新建项目

1.我们点击NEW,即可创建新项目

2.点击我们的项目就可以进入项目界面

3.点击EDITOR就可以进入可视化操作平台,去操作我们的项目

三、导出项目

1.导出项目前,我们得先pulish

2.点击下载 .zip 包

3.将包用vsCode打开,运行index.html

四、其他界面菜单

1.工具栏和菜单栏

点击 PLAYCANVAS 图标会显示所有的可用功能,可以使用这些功能编辑场景。如果找不到某个按钮或者快捷键,肯定是能在菜单里找到对应功能的。

工具栏提供快捷访问的常用功能,其中包含最重要的运行按钮。运行按钮会在一个独立的浏览器窗口中启动游戏并且加载当前的场景,可以立即开始游戏测试。可以从属性编辑栏中看到运行中游戏的实时参数变化。

2.层级树

场景是由层级结构的实体组成,场景层级树显示了当前整个场景的树形结构。一个场景永远包含一个根节点实体,位于层级树的最顶端。所有其他的实体都是由开发者逐步加入的。

层级树面板包含了场景中的全部实体,可以在其中快速定位实体,而不必在场景中搜寻。点击层级树上的实体将会选中它。

创建和删除实体

当在层级树中选中一个实体时,用户可以创建一个新的实体或者选出这个所选中的选项。或者使用位于层级树面板左上角的各种按钮,又或者通过点击操作打开内容目录来进行操作。

3.检测器

检查器面板会显示当前选定对象的属性值。

根据选择的不同,会现实不同的检查器面板。目前有

  • 实体/组件检查器
  • 纹理检查器
  • 材质检查器
  • Cubemap检查器

修改这些数值用以特制化实体的行为。譬如可以设置 Model 组件所渲染的模型,或灯光的颜色等灯。

有些参数是简单的文字或数值,通过标准的文字框和滑动条来进行修改。有些值需要特殊的编辑方式,譬如从资源面板中选取一个高亮的资源之类(选取纹理等)。有些值可以通过视口进行可视化编辑,譬如可以直接在视口中用Gizmo平移旋转缩放一个实体的变换属性.

在应用和编辑器同时运行的情况下,对实体属性的修改将会传递到运行中的应用中。一个绝佳的迭代方式是在应用运行起来之后,并排打开编辑器和应用窗口,实时的对想要的调整的值进行修正。

4.视口

视口展示了当前场景的可视化状态。你可以通过移动摄影机在场景中自由漫游。

摄影机

初始情况下,编辑器会采用 透视 摄影机。这种摄影机如同电影摄影机一般漂浮在场景之中。可以使用摄影机选择菜单选择场景中的不同摄影机。 正交 摄影机包含几个确定的机位:顶视图,底视图,前视图,后视图,左视图,右视图。通过这些机位可以无透视的观察场景,特别方便用来调整场景中的实体位置之类。

在摄影机菜单中选择摄影机的同时,场景中的摄影机实体会被显示为高亮状态。可以利用这个特性精确的设置摄影机位置。

5.资源

资源面板管理了所有的项目中的可用资源。从这个面板可以创建,上传,删除,查看和编辑任何资源

五、小结

three.js还是比PlayCanvas活跃的,当然,如果感兴趣并且有精力的话,还是建议都了解了解

毕竟知识是无穷无尽的

参考链接:

[playcanvas-github]: “https://github.com/playcanvas/engine/blob/main/README-zh.md
[playcanvas中文]: “https://developer.playcanvas.com/zh/

threejs-d-5.0

threejs-d-5.0

一、置换与置换贴图

.displacementMap : Texture

位移贴图会影响网格顶点的位置,与仅影响材质的光照和阴影的其他贴图不同,移位的顶点可以投射阴影,阻挡其他对象, 以及充当真实的几何体。位移纹理是指:网格的所有顶点被映射为图像中每个像素的值(白色是最高的),并且被重定位。

.displacementScale : Float

位移贴图对网格的影响程度(黑色是无位移,白色是最大位移)。如果没有设置位移贴图,则不会应用此值。默认值为1。

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/road_with_tram_rails_24_71_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/road_with_tram_rails_24_71_height.jpg') 
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.BoxGeometry( 1, 1, 1,100,100,100 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    displacementMap:mbHeighttexture,
    displacementScale:0.05

 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

置换效果图:

二、粗糙度与粗糙度贴图

.roughness : Float

材质的粗糙程度。0.0表示平滑的镜面反射,1.0表示完全漫反射。默认值为1.0。如果还提供roughnessMap,则两个值相乘。

.roughnessMap : Texture

该纹理的绿色通道用于改变材质的粗糙度。

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/road_with_tram_rails_24_71_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/road_with_tram_rails_24_71_height.jpg') 
const mbRoughnesstexture = textureLoader.load('./texture/road_with_tram_rails_24_71_roughness.jpg') 
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.BoxGeometry( 1, 1, 1,50,50,50 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    displacementMap:mbHeighttexture,
    displacementScale:0.05,
    // roughness:0,
    roughnessMap:mbRoughnesstexture

 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

粗糙度为0效果:

加载粗糙度纹理:

三、金属度与金属贴图

.metalness : Float

材质与金属的相似度。非金属材质,如木材或石材,使用0.0,金属使用1.0,通常没有中间值。 默认值为0.0。0.0到1.0之间的值可用于生锈金属的外观。如果还提供了metalnessMap,则两个值相乘。

.metalnessMap : Texture

该纹理的蓝色通道用于改变材质的金属度。

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_height.jpg') 
const mbRoughnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_roughness.jpg') 
const mbMetalnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_metalness.jpg') 

mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.PlaneGeometry( 1, 1,100,100 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    displacementMap:mbHeighttexture,
    displacementScale:0.05,
    roughness:0.2,
    roughnessMap:mbRoughnesstexture,
    metalnessMap:mbMetalnesstexture,
    metalness:0.3

 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

添加金属和金属贴图前:

添加金属和金属贴图后:

四、法线和法线贴图

.normalMap : Texture

用于创建法线贴图的纹理。RGB值会影响每个像素片段的曲面法线,并更改颜色照亮的方式。法线贴图不会改变曲面的实际形状,只会改变光照。 In case the material has a normal map authored using the left handed convention, the y component of normalScale should be negated to compensate for the different handedness.

.normalMapType : Integer

法线贴图的类型。

选项为THREE.TangentSpaceNormalMap(默认)和THREE.ObjectSpaceNormalMap。

.normalScale : Vector2

法线贴图对材质的影响程度。典型范围是0-1。默认值是Vector2设置为(1,1)

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_height.jpg') 
const mbRoughnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_roughness.jpg') 
const mbMetalnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_metalness.jpg') 
const mbNormaltexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_normal.jpg') 

mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.PlaneGeometry( 1, 1,100,100 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    displacementMap:mbHeighttexture,
    displacementScale:0.05,
    roughness:0.2,
    roughnessMap:mbRoughnesstexture,
    metalnessMap:mbMetalnesstexture,
    metalness:0.3,
    normalMap:mbNormaltexture

 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

添加法线贴图后:

贴图到这里就已经很逼真了,当然,不同的材质所使用的贴图也有一定的区别。

五、纹理加载回调

1.纹理回调

.load ( url : String, onLoad : Function, onProgress : Function, onError : Function ) : Texture

url — 文件的URL或者路径,也可以为 Data URI.
onLoad — 加载完成时将调用。回调参数为将要加载的texture.
onProgress — 将在加载过程中进行调用。参数为XMLHttpRequest实例,实例包含total和loaded字节。
onError — 在加载错误时被调用。

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_height.jpg') 
const mbRoughnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_roughness.jpg') 
const mbMetalnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_metalness.jpg') 
const event = {};
event.onLoad = function(e){
    console.log('加载完成了')
}
event.onProgress = function(e){
    console.log(e)
    console.log('正在加载')
}
event.onError = function(e){
    console.log('加载错误了')
    console.log(e)
}
const mbNormaltexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_normal.jpg',
event.onLoad,//加载完成调用
event.onProgress,//加载过程中调用
event.onError//加载错误调用
) 

mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.PlaneGeometry( 1, 1,100,100 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    displacementMap:mbHeighttexture,
    displacementScale:0.05,
    roughness:0.2,
    roughnessMap:mbRoughnesstexture,
    metalnessMap:mbMetalnesstexture,
    metalness:0.3,
    normalMap:mbNormaltexture

 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

加载中

onProgress

加载完成

onLoad

加载错误

onError

2.纹理加载器

其功能是处理并跟踪已加载和待处理的数据,管理我们加载的纹理

//main.js+
const event = {};
event.onLoad = function(e){
    console.log('加载完成了')
}
event.onProgress = function( url,itemsLoaded,itemsTotal){
    console.log('url:'+ url)//当前加载纹理路径
    console.log('itemsLoaded:' + itemsLoaded )//加载当前纹理位置
    console.log('itemsTotal:' + itemsTotal)//纹理总数
    console.log('加载角度:' + ((itemsLoaded / itemsTotal) * 100).toFixed(2) + '%')//纹理加载进度
}
event.onError = function(e){
    console.log('加载错误了')
    console.log(e)
}
const manager = new THREE.LoadingManager(
    event.onLoad,
    event.onProgress,
    event.onError
    );

const textureLoader = new THREE.TextureLoader(manager);//装载加载器
const mbColortexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_height.jpg') 
const mbRoughnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_roughness.jpg') 
const mbMetalnesstexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_metalness.jpg') 
const mbNormaltexture = textureLoader.load('./texture/welded_and_painted_metal_panels_26_64_normal.jpg') 
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.PlaneGeometry( 1, 1,100,100 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    displacementMap:mbHeighttexture,
    displacementScale:0.05,
    roughness:0.2,
    roughnessMap:mbRoughnesstexture,
    metalnessMap:mbMetalnesstexture,
    metalness:0.3,
    normalMap:mbNormaltexture

 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

六、环境纹理

创建一个由6张图片所组成的纹理对象。

(前后,上下,左右)也可以是 hdr(一张图)

const loader = new THREE.CubeTextureLoader();
loader.setPath( 'textures/cube/pisa/' );

const textureCube = loader.load( [
    'px.png', 'nx.png',
    'py.png', 'ny.png',
    'pz.png', 'nz.png'
] );
const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
scene.background = textureCube;
scene.environment = textureCube;//若该值不为null,则该纹理贴图将会被设为场景中所有物理材质的环境贴图。 然而,该属性不能够覆盖已存在的、已分配给 MeshStandardMaterial.envMap 的贴图

七、HDR

HDR

那么我们也是要知道如何加载hdr

高动态范围成像(英語:High Dynamic Range Imaging,简称HDRIHDR),在计算机图形学与电影摄影术中,是用来实现比普通数位图像技术更大曝光动态范围(即更大的明暗差别)的一组技术。高动态范围成像的目的就是要正确地表示真实世界中从太阳光直射到最暗的阴影这样大的范围亮度。

.mapping : number

图像将如何应用到物体(对象)上。默认值是THREE.UVMapping对象类型, 即UV坐标将被用于纹理映射。

EquirectangularReflectionMapping 和 EquirectangularRefractionMapping 用于等距圆柱投影的环境贴图,也被叫做经纬线映射贴图。等距圆柱投影贴图表示沿着其水平中线360°的视角,以及沿着其垂直轴向180°的视角。贴图顶部和底部的边缘分别对应于它所映射的球体的北极和南极。

//main.js+
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'
const rgbeLoader = new RGBELoader();
rgbeLoader.loadAsync('./texture/ruin1.hdr').then((texture)=>{
    texture.mapping = THREE.EquirectangularReflectionMapping;
    scene.background = texture;
    scene.environment = texture;
})
const geometry = new THREE.SphereGeometry( 15, 100, 50 );
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    metalness:0.5,
    roughness:0.05
 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

八、灯光与阴影

阴影需要满足的条件

1.材质应满足对灯光有反应(环境光由于是四面八方的光,所以不具备阴影的条件)

2.渲染器开启阴影计算:renderer.shadowMap.enabled = true

3.光照投射阴影:light.castShadow = true

4.物体投射阴影:cube.castShadow = true(投影主体)

5.物体接收投射阴影:plane.receiveShadow = true(接收主体)

//main.js
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 
import dat from 'dat.gui'

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;//渲染器开启阴影计算
document.body.appendChild( renderer.domElement );

const axesHelper = new THREE.AxesHelper( 30 );//轴的线段长度. 默认为 1.
scene.add( axesHelper );//添加到场景


const geometry = new THREE.SphereGeometry( 15, 100, 50 );
const material = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh( geometry, material );
cube.castShadow = true;//物体投射阴影
scene.add( cube );

const geometryP = new THREE.PlaneGeometry( 100,100 );
const materialP = new THREE.MeshStandardMaterial();
const plane = new THREE.Mesh( geometryP, materialP );
plane.rotation.x = -Math.PI / 2
plane.position.set(0,-15,0)
plane.receiveShadow = true//物体接收投射阴影
scene.add( plane );

const light = new THREE.PointLight( 0xffffff, 1, 100 );
light.position.set( 10, 50, 10 );
light.castShadow = true;//光照投射阴影
scene.add( light ); 

const gui = new dat.GUI();
gui.add(light.position,'x')
.min(0)
.max(30)
.name('移动x坐标')
.step(0.1)
gui.add(light.position,'y')
.min(0)
.max(30)
.name('移动y坐标')
.step(0.1)
gui.add(light.position,'z')
.min(0)
.max(30)
.name('移动z坐标')
.step(0.1)
const controls = new OrbitControls( camera,renderer.domElement  );
camera.position.set(0,20,100);
controls.update();
function animate() {
    requestAnimationFrame( animate );
    controls.update();
    renderer.render( scene, camera );
}
animate();

window.addEventListener('resize',()=>{
    // console.log('屏幕变化了')
    camera.aspect = window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
})

总结:

我们还有一些灯光,例如点光源和聚光灯,它们的属性我们就不一一来讲了,相信我们到了这,文档已经可以比较熟悉的查阅了。加油!!!

参考文档:

[HDR讲解]: “https://zh.m.wikipedia.org/zh/%E9%AB%98%E5%8A%A8%E6%80%81%E8%8C%83%E5%9B%B4%E6%88%90%E5%83%8F"

threejs-d-4.0

threejs-d-4.0

一、基础网格材质

一个以简单着色(平面或线框)方式来绘制几何体的材质。

这种材质不受光照的影响。

//main.js+
const textureLoader = new THREE.TextureLoader();//初始化一个加载器
const mbColortexture = textureLoader.load('./texture/mb.png') //加载一张纹理贴图

const geometry = new THREE.BoxGeometry( 1, 1, 1 );//创建一个立方体
const material = new THREE.MeshBasicMaterial( { 
    color: '#ffffff',//给立方体添加颜色
    map:mbColortexture//加载贴图
 } );
const cube = new THREE.Mesh( geometry, material );
console.log(cube)
.alphaMap : Texture 
#alpha贴图是一张灰度纹理,用于控制整个表面的不透明度。(黑色:完全透明;白色:完全不透明)。 默认值为null。

仅使用纹理的颜色,忽略alpha通道(如果存在)。 对于RGB和RGBA纹理,WebGL渲染器在采样此纹理时将使用绿色通道, 因为在DXT压缩和未压缩RGB 565格式中为绿色提供了额外的精度。 Luminance-only以及luminance/alpha纹理也仍然有效。

.aoMap : Texture
该纹理的红色通道用作环境遮挡贴图。默认值为null。aoMap需要第二组UV。

.aoMapIntensity : Float
环境遮挡效果的强度。默认值为1。零是不遮挡效果。

.color : Color
材质的颜色(Color),默认值为白色 (0xffffff)。

.combine : Integer
如何将表面颜色的结果与环境贴图(如果有)结合起来。

选项为THREE.MultiplyOperation(默认值),THREE.MixOperation, THREE.AddOperation。如果选择多个,则使用.reflectivity在两种颜色之间进行混合。

.envMap : Texture
环境贴图。默认值为null。

.fog : Boolean
材质是否受雾影响。默认为true。

.lightMap : Texture
光照贴图。默认值为null。lightMap需要第二组UV。

.lightMapIntensity : Float
烘焙光的强度。默认值为1。

.map : Texture
颜色贴图。可以选择包括一个alpha通道,通常与.transparent 或.alphaTest。默认为null。

.reflectivity : Float
环境贴图对表面的影响程度; 见.combine。默认值为1,有效范围介于0(无反射)和1(完全反射)之间。

.refractionRatio : Float
空气的折射率(IOR)(约为1)除以材质的折射率。它与环境映射模式THREE.CubeRefractionMapping 和THREE.EquirectangularRefractionMapping一起使用。 The index of refraction (IOR) of air (approximately 1) divided by the index of refraction of the material. It is used with environment mapping mode THREE.CubeRefractionMapping. 折射率不应超过1。默认值为0.98。

.specularMap : Texture
材质使用的高光贴图。默认值为null。

.wireframe : Boolean
将几何体渲染为线框。默认值为false(即渲染为平面多边形)。

.wireframeLinecap : String
定义线两端的外观。可选值为 'butt','round' 和 'square'。默认为'round'。

该属性对应2D Canvas lineJoin属性, 并且会被WebGL渲染器忽略。

.wireframeLinejoin : String
定义线连接节点的样式。可选值为 'round', 'bevel' 和 'miter'。默认值为 'round'。

该属性对应2D Canvas lineJoin属性, 并且会被WebGL渲染器忽略。

.wireframeLinewidth : Float
控制线框宽度。默认值为1。

效果展示:

二、纹理常用属性

const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/mb.png') 
console.log(mbColortexture)

我们可以看到mbColortexture的属性

1.纹理偏移

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/mb.png') 
mbColortexture.offset.set(0.2,0.2)//默认是x,y,范围是0.0 to 1.0

效果展示:

2.纹理旋转

const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/mb.png') 
mbColortexture.center.set(0.5,0.5)//设置中心点,(0.5, 0.5)对应纹理的正中心。默认值为(0,0),即左下角
mbColortexture.rotation = Math.PI / 4 //纹理将围绕中心点旋转多少度,单位为弧度(rad)。正值为逆时针方向旋转,默认值为0。

效果展示:

3.纹理重复

当我们旋转或者改变了uv属性,那么可能就出现了上面那种情况,是因为

.wrapS : number

这个值定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应于U
默认值是THREE.ClampToEdgeWrapping,即纹理边缘将被推到外部边缘的纹素。

我们需要修改属性,使其达到合适的uv

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/mb.png') 
mbColortexture.wrapS = THREE.RepeatWrapping//使用RepeatWrapping,纹理将简单地重复到无穷大。
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
mbColortexture.rotation = Math.PI / 4

//使用MirroredRepeatWrapping, 纹理将重复到无穷大,在每次重复时将进行镜像。

wrapS和wrapT可以设置相同的属性

.wrapS : number

这个值定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应于U
默认值是THREE.ClampToEdgeWrapping,即纹理边缘将被推到外部边缘的纹素。 其它的两个选项分别是THREE.RepeatWrapping和THREE.MirroredRepeatWrapping。 请参阅texture constants来了解详细信息。

.wrapT : number

这个值定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于V
可以使用与 .wrapS : number相同的选项。

请注意:纹理中图像的平铺,仅有当图像大小(以像素为单位)为2的幂(2、4、8、16、32、64、128、256、512、1024、2048、……)时才起作用。 宽度、高度无需相等,但每个维度的长度必须都是2的幂。 这是WebGL中的限制,不是由three.js所限制的。

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/mb.png') 
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.repeat.set(1,1)//设置重复的次数
mbColortexture.center.set(0.5,0.5)

三、纹理算法显示

这里主要是两个属性.magFilter和.minFilter

.magFilter : number

当一个纹素覆盖大于一个像素时,贴图将如何采样。默认值为THREE.LinearFilter, 它将获取四个最接近的纹素,并在他们之间进行双线性插值。 另一个选项是THREE.NearestFilter,它将使用最接近的纹素的值。
请参阅texture constants页面来了解详细信息。

.minFilter : number

当一个纹素覆盖小于一个像素时,贴图将如何采样。默认值为THREE.LinearMipmapLinearFilter, 它将使用mipmapping以及三次线性滤镜。

magFilter为THREE.NearestFilter效果展示:

magFilter为THREE.LinearFilter时效果展示:

.minFilter 的一些属性值

四、透明材质和纹理

alphaMap和opacity

//main.js
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/AHS_AlphaPack_V3_ (1).png') //加载颜色贴图
const mbAlphatexture = textureLoader.load('./texture/AHS_AlphaPack_V3_ (1).jpg') //加载Alpha贴图
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    alphaMap:mbAlphatexture,//alpha贴图是一张灰度纹理,用于控制整个表面的不透明度。(黑色:完全透明;白色:完全不透明)。 默认值为null。
    transparent:true //定义此材质是否透明。这对渲染有影响,因为透明对象需要特殊处 理,并在非透明对象之后渲染。设置为true时,通过设置材质的opacity属性来控制材质透明的程度。默认值为false。
    opacity:0.5 //设置材质的opacity属性来控制材质透明的程度
 } );
const cube = new THREE.Mesh( geometry, material ); 
scene.add( cube );

透明纹理效果展示:

五、环境遮挡贴图与强度

.aoMap : Texture

该纹理的红色通道用作环境遮挡贴图。默认值为null。aoMap需要第二组UV。

.aoMapIntensity : Float

环境遮挡效果的强度。默认值为1。零是不遮挡效果。

const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/road_with_tram_rails_24_71_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/road_with_tram_rails_24_71_height.jpg') 
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    aoMap:mbHeighttexture,
    aoMapIntensity:1//范围 0-1
 } );
const cube = new THREE.Mesh( geometry, material );
console.log(cube)

添加环境遮挡贴图前:

添加环境遮挡贴图后:

六、PBR

PBR是Physically-Based Rendering(基于物理的渲染)的简称,这个词的字面意思并不容易弄懂,到底什么是PBR呢?

让PBR材质流区别与其他手法的重要一点是光对表面造成的细腻效果,它有着更为准确的模拟,而且同时不失一定的艺术感。让我们从最基础的概念说起:

1.漫射与反射

这是两个材质制作里最基本的概念,是光与表面最基础的相互作用,很多人都知道这一点,但不一定知道在物理层面上到底是什么。

当一束光照射到一个表面,有些光会被反射,即从物体表面沿着法线反方向弹射回去。这一过程和一个球打在了墙上或地上反弹回去差不多。在一个光滑的表面上,这会产生一种镜面效果。

也不是所有的光都被物体表面反弹回去,有些会被物体表面吸收,或者散射进了物体内部,有些散射进去的光又会从后面再次穿出去,再次被眼睛或相机捕捉到。

-

光的这种被物体表面吸收和散射因光的不同波长而不同,也即颜色的不同而不同。由于这种散射极为不规则,所以看起来给人感觉是来自各个方向。这和镜面效果很不同!要在材质里模拟这种效果,可以通过定义一个漫射颜色来做到。

2.半透与透明

但有些漫射很复杂,比如皮肤和蜡烛的表面,简单的定义一个漫射颜色是不够的,还需要考虑材质的厚度和形状。如果物体本身很薄,那么就应该能看到透明效果。这时就需要特殊材质才能模拟了。

能量守恒(一)
通过以上对反射和漫射的描述,我们不难得出一个结论:漫射和反射是互斥的。为了能看到漫射颜色,光必须先穿过表面,或者说没能被表面反射回去。那么根据能量守恒法则,就出现这样的情况:越是反射强烈的表面,其漫射越是弱(暗),因为没有那么多的光穿过去,大部分都被反射回去了。一个表面很明亮的物体它不可能有很强的反射。

-

能量守恒是PBR材质流的基础,要做出自然逼真的材质来,不可以无视这个规则。

3.金属

导电体,比如金属材质要特别单独拿出来讲一下。因为:
首先,它们比绝缘体的表面更加具有反射效果,它们的反射率高达60-90%,而绝缘体只有0-20%。高反射率让光无法穿过表面进入物体内部,从而产生了闪光的质感。

其次,金属的反射效果还会随着光谱而不同,这就是说,金属的反射是带有颜色的,而且很不均匀,比如金,铜等。而绝缘体的反射是无色的。

最后,穿过导电体表面的光最后大部分也会被吸收而不是被散射出去,这既是说,理论上导电体是没有漫射颜色的。

4.菲涅尔

奥古斯汀-让·菲涅尔,这个人的名字是绝对不可能被遗忘的,因为他的名字和反射现象结合到了一起去,不说他的名字谈反射几乎不可能。

在CG里,菲涅尔代表了反射效果因观察角度的不同而不同,也即是物体边缘的反射效果会更加明亮。PBR材质流在这个效果上有几个特色:

首先,对于所有的材质来说,反射效果在接近0度角时最明显,就像镜面反射一样,不管是什么材质都是如此。只要足够光滑能产生反射,那么一定能在某一个角度观察时,看到完美的镜面反射效果!这听起来有点玄乎,但这就是物理。

其次,这种反射效果的变化随着材质的不同而有所不同,如下图:

-

-

-

这个图的意思是在说:在模拟真实材质时,反射效果必须要稍微有些收敛,至少要在需要弱的地方弱下来。这并不难做到,而且现在的材质系统都能自带这种菲尼尔效果了。PBR材质流是可以带出些艺术处理的,在做出基础反射效果后,你可以再添加一些完全反射的小局部做点缀。

5.微表面

上面都是基于物体表面说的东西,或者说是物体能被渲染出来的效果。但物体表面并非完美无瑕,小凹痕,裂纹,凹凸,这些无法被眼睛看到,也无法在法线贴图里被体现出来的微小细节,确实左右漫射和反射效果的真正因素。

-

-

-

上图中,平行照射到物体表面的三道光线被反射到不同的方向上去了,因为它们所照到的物体表面具有不同的方向,使得这种反射的角度变得不可预测,这就产生了模糊反射效果。

但要从物理级别上模拟出这种无规则的反射需要大量的内存和计算量,在CG里只能大概模拟一个整体效果,我们一般叫反射光泽度或反射模糊度。

这是材质制作里一个重要的部分,因为真实世界里到处充满了这种模糊反射效果。

能量守恒(二)
现在渲染器都有了这种虚拟的模糊反射的模拟,正确的参数能让效果更加逼真,比如粗糙的表面会产生更大更柔和的高光区,请看下图:

05.png

-

-

-

图中的小球具有相同的反射强度,但因为不同的反射模糊度而表现出不同的质感,高光随着光滑度越来越清晰明亮。这又是能量守恒的法则体现之一。

微表面反射也同样遵守菲涅尔效果

-

-

-

PBR材质似乎在一夜之间改变了人们对于引擎实时渲染画面的理解。在游戏中我们也能够体验到锈蚀的金属,厚重的皮革,精细的纹理,更加真实的世界从此展现在眼前。从此,PBR材质成为了“次时代”游戏必备的标准。

七、标准网格材质

一种基于物理的标准材质

基于物理的渲染(PBR)这种方法与旧方法的不同之处在于,不使用近似值来表示光与表面的相互作用,而是使用物理上正确的模型。 我们的想法是,不是在特定照明下调整材质以使其看起来很好,而是可以创建一种材质,能够“正确”地应对所有光照场景。

我们PBR材质是和光相辅相成的

1.PointLight( color : Integer, intensity : Float, distance : Number, decay : Float )

color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)。
intensity - (可选参数) 光照强度。 缺省值 1。
distance - 这个距离表示从光源到光照强度为0的位置。 当设置为0时,光永远不会消失(距离无穷大)。缺省值 0.
decay - 沿着光照距离的衰退量。缺省值 2

2.AmbientLight( color : Integer, intensity : Float )

color - (参数可选)颜色的rgb数值。缺省值为 0xffffff。
intensity - (参数可选)光照的强度。缺省值为 1。

//main.js+
const textureLoader = new THREE.TextureLoader();
const mbColortexture = textureLoader.load('./texture/road_with_tram_rails_24_71_diffuse.jpg') 
const mbHeighttexture = textureLoader.load('./texture/road_with_tram_rails_24_71_height.jpg') 
mbColortexture.wrapS = THREE.RepeatWrapping
mbColortexture.wrapT = THREE.RepeatWrapping
mbColortexture.center.set(0.5,0.5)
// mbColortexture.rotation = Math.PI / 4
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
//和基础网格材质一样具备相同属性
const material = new THREE.MeshStandardMaterial( { 
    color: '#ffffff',
    map:mbColortexture,
    aoMap:mbHeighttexture,
    aoMapIntensity:1
 } );
const cube = new THREE.Mesh( geometry, material );
console.log(cube)
scene.add( cube );

// const light = new THREE.AmbientLight( 0xffffff); // 环境光,相当于各个方向射过来的光(没有投影)
// scene.add( light );

const light = new THREE.PointLight( 0xffffff, 5, 100 );//点光源,相当于灯泡,相对灯光的物体背面是没光的
light.position.set( 10, 10, 10 );
scene.add( light );

参考链接:

[什么是PBR]: “https://tao0.date/2021/06/shenmeshipbrcaizhi/

threejs-d-3.0

threejs-d-3.0

一、通过Clock跟踪时间处理动画

我们通过时间属性去控制物体动画,我们需要一个对象用于跟踪时间

const clock = THREE.Clock();
const time = clock.getElapsedTime();//获取时钟运行的总时长
const getDelta = clock.getDelta();//获取两次渲染的间隔时长

Clock( autoStart : Boolean )

autoStart — (可选) 是否要在第一次调用 .getDelta() 时自动开启时钟。默认值是 true

属性

.autoStart : Boolean

如果设置为 true,则在第一次调用 .getDelta() 时开启时钟。默认值是 true

.startTime : Float

存储时钟最后一次调用 start 方法的时间。默认值是 0

.oldTime : Float

存储时钟最后一次调用 start, .getElapsedTime() 或 .getDelta() 方法的时间。默认值是 0

.elapsedTime : Float

保存时钟运行的总时长。默认值是 0

.running : Boolean

判断时钟是否在运行。默认值是 false

方法

.start () : undefined

启动时钟。同时将 startTime 和 oldTime 设置为当前时间。 设置 elapsedTime 为 0,并且设置 running 为 true.

.stop () : undefined

停止时钟。同时将 oldTime 设置为当前时间。

.getElapsedTime () : Float

获取自时钟启动后的秒数,同时将 .oldTime 设置为当前时间。
如果 .autoStart 设置为 true 且时钟并未运行,则该方法同时启动时钟。

.getDelta () : Float

获取自 .oldTime 设置后到当前的秒数。 同时将 .oldTime 设置为当前时间。
如果 .autoStart 设置为 true 且时钟并未运行,则该方法同时启动时钟。

二、Gsap动画库的基本使用

这个动画库的API都有相关的动画示例,所以即使看不懂文档的同学使用也是可以明白的

1.安装并使用

npm install gsap //安装gsap
//main.js
import gsap from 'gsap'
gsap.to(cube.position,{x:20,duration:5})//动画属性,控制物体位置和运动时间(duration)
gsap.to(cube.rotation,{x:Math.PI/4,duration:5})//动画属性,控制物体旋转和运动时间(duration)

当然我们的动画运动肯定不只是匀速运动,也是可以配置我们的缓冲器(和贝赛尔曲线差不多)

gsap.to(cube.position,{x:20,duration:5,ease: "elastic.out(1, 0.2)"})

2.其他属性使用

我们也可以在动画开始、更新、结束去执行回调

我们gsap.to()还有其他动画的属性,比如往返运动、定义关键帧、循环等等

//main.js
import gsap from 'gsap'
gsap.to(cube.position,{
    x:20,
    duration:5,
    ease: "elastic.out(1, 0.2)",
    onStart:()=>{console.log('动画开始')},
    onComplete:()=>{console.log('动画结束')},
    onUpdate:()=>{console.log('更新动画')}
})

我们还可以根据返回对象实现对动画的控制

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>threejs</title>
</head>
<body>
    <div class="nav">
        <button id="play">play()</button>
        <button id="pause">pause()</button>
        <button id="resume">resume()</button>
        <button id="reverse">reverse()</button>
        <button id="restart">restart()</button>
      </div>
    <script src="./main.js"></script>
</body>
</html>
const tween = gsap.to(cube.position,{
    x:20,
    duration:5,
    ease: "elastic.out(1, 0.2)",
    onStart:()=>{console.log('动画开始')},
    onComplete:()=>{console.log('动画结束')},
    onUpdate:()=>{console.log('更新动画')}
})

document.querySelector("#play").onclick = () => tween.play();//开启动画
document.querySelector("#pause").onclick = () => tween.pause();//暂停动画
document.querySelector("#resume").onclick = () => tween.resume();//继续播放动画
document.querySelector("#reverse").onclick = () => tween.reverse();//反转动画
document.querySelector("#restart").onclick = () => tween.restart();//重新开始

三、自适应更改

我们希望我们的画面能够适应我们屏幕的更改

//main.js+
//监听屏幕变化
window.addEventListener('resize',()=>{
  //更新摄影机
    camera.aspect = window.innerWidth/window.innerHeight;
  //更新摄影机的投影矩阵
    camera.updateProjectionMatrix();
  //更新渲染器
    renderer.setSize(window.innerWidth,window.innerHeight);
  //更新渲染器的像素比
    renderer.setPixelRatio(window.devicePixelRatio);
})

四、可视化调节

我们在开发过程中,各种参数比较多,我们需要改一下看一下效果是否符合我们的需要,这样我们的效率就会降低。

所以我们需要一款能快速调节参数的可视化工具dat.gui

1.安装

npm install --save dat.gui

2.使用

修改数值

import dat from 'dat.gui'//引入
const gui = new dat.GUI();//初始化

gui.add(cube.position,'x')//添加要控制的属性
.min(0)//最小值
.max(20)//最大值
.name('移动x坐标')//控制器名称
.step(0.1)//改变刻度
.onChange(value=>console.log('x坐标改变了:'+value))//坐标改变触发
.onFinishChange(value=>console.log('x坐标停止改变了:'+value))//坐标停止改变触发

修改颜色

const parm = {
    color:'#ffffff'
}
gui.addColor(parm,'color')
.name('物体颜色')
.onChange(value=>{
    cube.material.color.set(value)
})

我们的dat.gui还有许多API可以调用,具体的我们可以看dat.gui-api

五、BufferGeometry创建顶点创建矩形

我们所见的立方体,是由顶点坐标创建的面形成的

也可以看webgl里有一些基础的知识可以帮助我们理解点与面的关系

官方描述:是面片、线或点几何体的有效表述。包括顶点位置,面片索引、法相量、颜色值、UV 坐标和自定义缓存属性值。使用 BufferGeometry 可以有效减少向 GPU 传输上述数据所需的开销。

//main.js
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 


const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const axesHelper = new THREE.AxesHelper( 30 );//轴的线段长度. 默认为 1.
scene.add( axesHelper );//添加到场景

const geometry = new THREE.BufferGeometry();
// 创建一个简单的矩形. 在这里我们左上和右下顶点被复制了两次。
// 因为在两个三角面片里,这两个顶点都需要被用到。
const vertices = new Float32Array( [
    -1.0, -1.0,  1.0,
     1.0, -1.0,  1.0,
     1.0,  1.0,  1.0,

     1.0,  1.0,  1.0,
    -1.0,  1.0,  1.0,
    -1.0, -1.0,  1.0
] );
// itemSize = 3 因为每个顶点都是一个三元组。
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
console.log(cube)

 
scene.add( cube );
const controls = new OrbitControls( camera,renderer.domElement  );
camera.position.set(0,20,100);
controls.update();
function animate() {
   
    requestAnimationFrame( animate );
    controls.update();
    renderer.render( scene, camera );
}
animate();

window.addEventListener('resize',()=>{
    // console.log('屏幕变化了')
    camera.aspect = window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
})

参考链接:

[GSAP]: “https://github.com/greensock/GSAP
[gsap]: “https://greensock.com/gsap/
[Cubic Bezier]: “https://cubic-bezier.com/#.17,.67,.83,.67
[dat.gui]: “https://www.npmjs.com/package/dat.gui
[dat.gui-api]: “https://github.com/dataarts/dat.gui/blob/master/API.md

threejs-d-2.0

threejs-d-2.0

一、创建第一个threejs应用

代码汇总:

//main.js
import * as THREE from 'three'

const scene = new THREE.Scene();//创建场景
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );//创建透视相机(相机中的一种)
const renderer = new THREE.WebGLRenderer();//创建渲染器
renderer.setSize( window.innerWidth, window.innerHeight );//将输出canvas的大小调整为(width, height)并考虑设备像素比,且将视口从(0, 0)开始调整到适合大小
document.body.appendChild( renderer.domElement );//将renderer(渲染器)的dom元素(renderer.domElement)添加到我们的HTML文档中

const geometry = new THREE.BoxGeometry( 1, 1, 1 );//创建几何体
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );//创建基础网格材质
const cube = new THREE.Mesh( geometry, material );//使用构造器
scene.add( cube );//将几何体添加到场景
camera.position.z = 5;//调整相机位置(默认0,0,0)

//几何体循环旋转动画
function animate() {
    requestAnimationFrame( animate );
  //旋转坐标变换
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
  //重新渲染几何体
    renderer.render( scene, camera );
}
animate();

1.创建场景、相机、渲染器

如果我们有玩过三维软件的同学应该不会陌生这几个概念

我们下面以三维软件C4D来简单认识一下吧

我们花一点点时间来解释一下这里发生了什么。我们现在建立了场景、相机和渲染器。

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

three.js里有几种不同的相机,在这里,我们使用的是PerspectiveCamera(透视摄像机)。

第一个参数是视野角度(FOV)。视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的单位是角度(与弧度区分开)。

第二个参数是长宽比(aspect ratio)。 也就是你用一个物体的宽除以它的高的值。比如说,当你在一个宽屏电视上播放老电影时,可以看到图像仿佛是被压扁的。

接下来的两个参数是近截面(near)和远截面(far)。 当物体某些部分比摄像机的远截面远或者比近截面近的时候,该这些部分将不会被渲染到场景中。或许现在你不用担心这个值的影响,但未来为了获得更好的渲染性能,你将可以在你的应用程序里去设置它。

接下来是渲染器。这里是施展魔法的地方。除了我们在这里用到的WebGLRenderer渲染器之外,Three.js同时提供了其他几种渲染器,当用户所使用的浏览器过于老旧,或者由于其他原因不支持WebGL时,可以使用这几种渲染器进行降级。

除了创建一个渲染器的实例之外,我们还需要在我们的应用程序里设置一个渲染器的尺寸。比如说,我们可以使用所需要的渲染区域的宽高,来让渲染器渲染出的场景填充满我们的应用程序。因此,我们可以将渲染器宽高设置为浏览器窗口宽高。对于性能比较敏感的应用程序来说,你可以使用setSize传入一个较小的值,例如window.innerWidth/2window.innerHeight/2,这将使得应用程序在渲染时,以一半的长宽尺寸渲染场景。

如果你希望保持你的应用程序的尺寸,但是以较低的分辨率来渲染,你可以在调用setSize时,将updateStyle(第三个参数)设为false。例如,假设你的 标签现在已经具有了100%的宽和高,调用**setSize(window.innerWidth/2, window.innerHeight/2, false)**将使得你的应用程序以一半的分辨率来进行渲染。

document.body.appendChild( renderer.domElement );

最后一步很重要,我们将renderer(渲染器)的dom元素(renderer.domElement)添加到我们的HTML文档中。这就是渲染器用来显示场景给我们看的元素。

2.创建几何体、材质

对于材质这块,就是给一个没有任何属性的物体添加属性,玻璃、金属、玉、有颜色的立方体。

要创建一个立方体,我们需要一个BoxGeometry(立方体)对象. 这个对象包含了一个立方体中所有的顶点(vertices)和面(faces)。未来我们将在这方面进行更多的探索。

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
camera.position.z = 5;

接下来,对于这个立方体,我们需要给它一个材质,来让它有颜色。Three.js自带了几种材质,在这里我们使用的是MeshBasicMaterial。所有的材质都存有应用于他们的属性的对象。在这里为了简单起见,我们只设置一个color属性,值为0x00ff00,也就是绿色。这里所做的事情,和在CSS或者Photoshop中使用十六进制(hex colors)颜色格式来设置颜色的方式一致。

第三步,我们需要一个Mesh(网格)。 网格包含一个几何体以及作用在此几何体上的材质,我们可以直接将网格对象放入到我们的场景中,并让它在场景中自由移动。

默认情况下,当我们调用**scene.add()的时候,物体将会被添加到(0,0,0)**坐标。但将使得摄像机和立方体彼此在一起。为了防止这种情况的发生,我们只需要将摄像机稍微向外移动一些即可。

3.创建渲染

这个就比较好理解了,添加x,y,z方向的位置变化

现在,如果将之前写好的代码复制到HTML文件中,你不会在页面中看到任何东西。这是因为我们还没有对它进行真正的渲染。为此,我们需要使用一个被叫做“渲染循环”(render loop)或者“动画循环”(animate loop)的东西。

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

在这里我们创建了一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒)。如果你是一个浏览器游戏开发的新手,你或许会说“为什么我们不直接用setInterval来实现刷新的功能呢?”当然啦,我们的确可以用setInterval,但是,requestAnimationFrame有很多的优点。最重要的一点或许就是当用户切换到其它的标签页时,它会暂停,因此不会浪费用户宝贵的处理器资源,也不会损耗电池的使用寿命。

在开始之前,如果你已经将上面的代码写入到了你所创建的文件中,你可以看到一个绿色的方块。让我们来做一些更加有趣的事 —— 让它旋转起来。

将下列代码添加到animate()函数中renderer.render调用的上方:

cube.rotation.x += 0.01; 
cube.rotation.y += 0.01;

这段代码每帧都会执行(正常情况下是60次/秒),这就让立方体有了一个看起来很不错的旋转动画。基本上来说,当应用程序运行时,如果你想要移动或者改变任何场景中的东西,都必须要经过这个动画循环。当然,你可以在这个动画循环里调用别的函数,这样你就不会写出有上百行代码的animate函数。

效果演示:

二、控制器(轨道控制器)

控制器有许多,我们这先说这一种

轨道控制器是以物体为中心,对物体做环绕运动

代码汇总:

//main.js
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' //导入轨道控制器

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry( 5, 5, 5 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
const controls = new OrbitControls( camera,renderer.domElement  );
camera.position.set(0,20,100);
controls.update();//更新控制器。必须在摄像机的变换发生任何手动改变后调用
function animate() {
    requestAnimationFrame( animate );
    controls.update();//循环更新轨道控制器
    renderer.render( scene, camera );
}
animate();

OrbitControls( object : Camera, domElement : HTMLDOMElement )

object: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。

domElement: 用于事件监听的HTML元素。

那我们现在就可以移动我们的相机来观察我们的物体了。

三、添加辅助器

这个来自与我们help里面的东西,文档里查就可以了

添加辅助器的用处是为了更好的去开发

1.坐标辅助器

用于简单模拟3个坐标轴的对象.
红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.

const axesHelper = new THREE.AxesHelper( 5 );//轴的线段长度. 默认为 1.
scene.add( axesHelper );//添加到场景

2.相机辅助器

用于模拟相机视锥体的辅助对象.
它使用 LineSegments 来模拟相机视锥体.

const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const helper = new THREE.CameraHelper( camera );
scene.add( helper );

CameraHelper( camera : Camera )

camera – 被模拟的相机.

为指定相机创建一个新的相机辅助对象 CameraHelper .

3.平面辅助器

用于模拟平面 Plane 的辅助对象.

const plane = new THREE.Plane( new THREE.Vector3( 1, 1, 0.2 ), 3 );
const helper = new THREE.PlaneHelper( plane, 100, 0xffff00 );
scene.add( helper );

PlaneHelper( plane : Plane, size : Float, hex : Color )

plane – 被模拟的平面.
size – (可选的) 辅助对象的单边长度. 默认为 1.
color – (可选的) 辅助对象的颜色. 默认为 0xffff00.

创建一个线框辅助对象来表示指定平面.

终结:

其他的辅助器,我们就自己按照需求去加吧,这个就是做一个参照

四、物体的属性

物体的属性,那我们要看一下cube是什么了

根据控制台打印,它是个Mesh类,下面有许多属性

const geometry = new THREE.BoxGeometry( 5, 5, 5 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

1.物体的位移

控制物体的位置

cube.position.set(10,0,0)//默认为x,y,z方向

2.物体的旋转

控制问题的旋转角度

这个不是单纯的三维向量了

是一个Euler类

cube.rotation.set(Math.PI/4,0,0,'XYZ')//默认为‘xyz’

Euler( x : Float, y : Float, z : Float, order : String )

x - (optional) 用弧度表示x轴旋转量。 默认值是 0
y - (optional) 用弧度表示y轴旋转量。 默认值是 0
z - (optional) 用弧度表示z轴旋转量。 默认值是 0
order - (optional) 表示旋转顺序的字符串,默认为’XYZ’(必须是大写)。

3.物体的缩放

控制物体缩放

cube.scale.set(3,2,1)

electron-vue3-vite

Electron-Vue3-Vite

今天这一篇文章算是electron的一个短暂的结尾,以后我们再更新electron 的知识。

今天的内容是项目的vue3+electron项目搭建和打包部署

一、Electron-Vue3-Vite搭建

1.创建模板

!!!兼容性注意

Vite 需要 Node.js版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。

//使用npm
npm create vite@latest
//使用yarn
yarn create vite
//使用pnpm
pnpm create vite

使用以下命令启动:

npm i //安装依赖
npm run dev //启动项目

2.安装electron

npm i electron -D 

目前博主遇到了一个小问题,就是下载electron会一直卡住,这里使用如下方法就可以解决了。

npm install cnpm -g --registry=https://registry.npm.taobao.org //切换国内镜像源
cnpm isntall electron --save-dev

3.创建入口文件

根目录下创建

Main.js入口文件

//main.js
const { app,BrowserWindow } = require('electron')
const path = require('path')
const WinState = require('electron-win-state').default //保存窗口状态的包(窗口可以保存缩放、位置状态)

const createWindow = () => {
  const winState = new WinState({ 
    defaultWidth: 1000,
    defaultHeight: 800,
    // other winState options, see below
  })
    const win = new BrowserWindow({
      ...winState.winOptions,
      // width: 800,
      // height: 600,
      webPreferences: {
        preload: path.join(__dirname, 'preload.js')
      }
    })
    win.loadURL('http://127.0.0.1:5173/') //加载本地端口,部署的时候可以换成我们的域名
    win.webContents.openDevTools() //打开控制台调试
    winState.manage(win)
  }

  app.whenReady().then(() => {
    createWindow()
    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  }) 

  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })

Preload.js预加载文件

//目前先不写内容,先搭个架子

配置package.json

//package.json
{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "main": "main.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "start": "nodemon --exec electron . --watch ./ --ext .js,.html,.vue,.css",
  },
  "dependencies": {
    "electron-win-state": "^1.1.22",
    "vue": "^3.2.41"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.2.0",
    "electron": "^21.3.1",
    "electron-builder": "^23.6.0",
    "nodemon": "^2.0.20",
    "vite": "^3.2.3"
  }
}

4.安装其他依赖

cnpm install nodemon --save-dev //安装nodemon
cnpm install electron-win-state --save-dev //安装electron-win-state

5.运行项目

npm run dev //启动vue项目
npm start //启动electron

我们现在就可以去开发我们的项目了

二、打包与部署

官网推荐我们的是Electron Forge来分发我们的应用程序

electron-forge 可以自动检测你的系统,然后打包成对应的可执行文件。
它可以实现 package 成最终可用的独立项目文件夹,
还可以 make 成能够安装的 zip 包

目前这个方法有一些问题,我们先用另一种方法来进行打包electron-build

根据文档我们也可以很快的掌握这种打包方式

1.安装依赖

cnpm i electron-builder -D //安装依赖

配置一下package.json

//package.json+
"scripts": {
  ...
  "app:dir": "electron-builder --dir",
  "app:dist": "electron-builder"
  ...
}

2.打包

cnpm run app:dist  //执行打包

目前我遇到打包错误的问题

如果我们遇到这种问题,我们切换一下npm镜像源就可以了;

现在我们打包就成功了

3.运行安装包

找到我们项目目录下的dist文件的.dmg安装包(我的是mac)

在运行之前,我们应该先将项目 运行 起来(因为我们是本地项目,等部署到服务器就不用了)

点击执行安装包

到目前为止,我们的electron的搭建->开发->打包就一气呵成了,当然,开发项目中还有许多奇奇怪怪的问题和知识点,就靠各位聪明的脑瓜了。。

参考链接:

[Electron官网]: “https://www.electronjs.org/docs/latest/tutorial/forge-overview
[Vite.js官网]: “https://cn.vitejs.dev/
[electron-builder]: “https://github.com/electron-userland/electron-builder

threejs-d-1.0

threejs-d-1.0

d-1.0主要是准备我们开发环境和其他的一些准备,为了我们更好的去进入three的世界

一、搭建three.js本地网站

搭建本地网站的原因,是因为我们访问官网的时候,由于网速限制,导致我们的体验感下降,搭建本地服务器,快很多了。

我们打开官网Three.js点击github就进入了threejs仓库了

我们下载仓库的dev开发包

git clone 和 下载压缩包都可以!

执行命令运行:

npm i //安装依赖
npm start //运行项目

我们现在访问本地项目就快多了,我们想知道任何一个例子也可以查看其具体的文件了。

二、使用parcel搭建threejs开发环境

npm init -y //初始化项目
npm install  parcel-bundler -D //安装parcel

设置入口文件main.js和index.html

配置package.json

//package.json
{
  "name": "threejs",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "parcel ./src/index.html",
    "build": "parcel build ./src/index.html"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "parcel-bundler": "^1.12.5"
  }
}

下载threejs依赖

npm install three --save //安装依赖
//main.js
import * as THREE from 'three'
console.log(THREE)

运行项目,打包项目

npm run dev //运行项目
npm run build //打包项目

运行成功!!!

参考链接:

[Three.js官网]: “https://threejs.org/
[Three.js-github]: “https://github.com/mrdoob/three.js/
[parcel官网]: “https://parceljs.org/"
[parcel中文官网]: “https://www.parceljs.cn/

Electron-d-3.0

Electron-d-3.0

渲染进程

上一篇我们讲解了主进程的一些常用API,这一篇我们接着来讲解渲染进程的API

1.剪切板(clipboard)

在系统剪贴板上执行复制和粘贴操作。

主进程和渲染进程都可以用。

(1)clipboard.readText([type])

  • type string (optional) -可以是 selectionclipboard; 默认为 ‘clipboard’. selection 仅在 Linux 中可用。

返回 string - 剪贴板中的内容为纯文本。

const { clipboard } = require('electron')

clipboard.writeText('hello i am a bit of text!')

const text = clipboard.readText()
console.log(text)
// hello i am a bit of text!'

(2)clipboard.writeText(text[, type])

  • text string
  • type string (optional) -可以是 selectionclipboard; 默认为 ‘clipboard’. selection 仅在 Linux 中可用。

text 作为纯文本写入剪贴板。

const { clipboard } = require('electron')

const text = 'hello i am a bit of text!'
clipboard.writeText(text)

我们剪切板的API就说这两个读与写。但是我在渲染进程里读不到clipboard。

[clipboard文档]: “https://www.electronjs.org/zh/docs/latest/api/clipboard

2.contextBridge

在隔离的上下文中创建一个安全的、双向的、同步的桥梁。

如下,是一个从隔离的预加载脚本将 API 暴露给渲染器的示例:(通过预加载脚本暴露API,渲染进程可以读取)

// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld(
  'electron',
  {
    doThing: () => ipcRenderer.send('do-a-thing')
  }
)
// Renderer (Main World)

window.electron.doThing()

[contextBridge文档]: “https://www.electronjs.org/zh/docs/latest/api/context-bridge

3.desktopCapturer

官方文档给的案例不是特别容易理解,代码有点长。

虽然官方文档的主进程和渲染进程都有desktopCapturer,但是渲染进程是访问不到的

我们举一个抓取页面内容的荔枝吧!

//main.js+
 ipcMain.handle('capture-event',async (event,args)=>{
      return  desktopCapturer.getSources({ types: ['window', 'screen'] }).then( sources => {
        return sources
      })
 })
//解释一下,desktopCapturer.getSources返回的是一个Promise的对象
//preload.js+

  const capture = async ()=>{
    let res = await ipcRenderer.invoke('capture-event')
    console.log(res)
  }
  
  contextBridge.exposeInMainWorld('electronAPI', {
    capture
})
//app.js+
const btn = document.getElementById('btn')
btn.addEventListener('click',()=>{
    window.electronAPI.capture()
})
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>capture</title>
  </head>
  <body>
    <button id="btn">capture</button>
    <script src='./renderer/app.js'></script>
  </body>
</html>

当我们点击页面上的按钮的时候,我们就会打印出窗口信息,由下图可以看出sources是一个数组

我们现在就可以根据我们获取到的信息去干一些事情了。

//preload.js+
  const capture = async ()=>{
    let sources = await ipcRenderer.invoke('capture-event')
    for (const source of sources) {
      if (source.name === 'electron') {
        let str = source.thumbnail.crop({x:0,y:0,width:300,height:300})
        let imgUrl = str.toDataURL()
        return imgUrl //返回的是一个Promise
      }
    }
  }

  contextBridge.exposeInMainWorld('electronAPI', {
    handleCounter: (callback) => ipcRenderer.on('update-counter', callback),
    capture,
})
//app.js+
const btn = document.getElementById('btn')
btn.addEventListener('click',async ()=>{
   let imgUrl = await window.electronAPI.capture()
   console.log(imgUrl)
})

我们在渲染进程这就已经拿到了我们的数据

接下来我们就将我们的数据填充到我们的页面之中

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';img-src * data:">
    <title>Menu Counter</title>
  </head>
  <body>
    <!-- Current value: <strong id="counter">0</strong> -->
    <button id="btn">capture</button>
    <img src="" alt="捕获数据" id="img">
    <script src='./renderer/app.js'></script>
  </body>
</html>
//app.js+
const btn = document.getElementById('btn')

btn.addEventListener('click',async ()=>{
   let imgUrl = await window.electronAPI.capture()
   document.getElementById('img').src = imgUrl
})

恭喜你!!!成功了🥳🥳🥳

[desktopCapturer文档]: “https://www.electronjs.org/zh/docs/latest/api/desktop-capturer"

4.nativeImage

使用 PNG 或 JPG 文件创建托盘、dock和应用程序图标。

主进程和渲染进程都可以用。

在 Electron 内, 那些需要图片的 API 可以传递两种参数, 一种是文件路径, 一种是 NativeImage 实例对象。 空的图片对象将被 null 参数替代

例如, 创建托盘或设置窗口图标时, 你可以传递 string 格式的图片路径

const { BrowserWindow, Tray } = require('electron')

const appIcon = new Tray('/Users/somebody/images/icon.png')
const win = new BrowserWindow({ icon: '/Users/somebody/images/window.png' })
console.log(appIcon, win)

或者从粘贴板读取图片,将返回 NativeImage 对象:

const { clipboard, Tray } = require('electron')
const image = clipboard.readImage()
const appIcon = new Tray(image)
console.log(appIcon)

我们文档里还有其他的一些关于NativeImage对象的属性和方法,我们就哪里需要哪里搬。

[nativeImage文档]: “https://www.electronjs.org/zh/docs/latest/api/native-image

到目前为止,我们对于electron的学习先到这,如果我的这几篇文章都认认真真的看完了,那么我们也对electron有了进一步认识。

参考链接:

[Electron官网]: “https://www.electronjs.org/

Electron-d-2.0

Electron

本篇文章接上一篇文章,详细讲解主进程和渲染进程一些常用的API

主进程API

1.App+BrowserWindow

App事件:
(1)Before-quit
  • event Event #返回

在程序关闭窗口前触发(macOS不是关闭窗口触发,而是关闭程序)

**注:**在 Windows 系统中,如果应用程序因系统关机/重启或用户注销而关闭,那么这个事件不会被触发。

代码以及程序演示:

(2)browser-window-blur
  • event Event #返回
  • window BrowserWindow #返回

当一个 browserWindow (窗口)失去焦点时触发。当我们鼠标点击窗口以外的地方就会失焦。

代码以及程序演示:

(3)browser-window-focus
  • event Event #返回
  • window BrowserWindow #返回

当一个 browserWindow 获得焦点时触发。

代码以及程序演示:

(4)quit
  • event Event #返回
  • exitCode Integer #返回

在应用程序退出时发出。

**注:**在 Windows 系统中,如果应用程序因系统关机/重启或用户注销而关闭,那么这个事件不会被触发。

代码以及程序演示:

(5)window-all-closed

当所有的窗口都被关闭时触发。

如果你没有监听此事件并且所有窗口都关闭了,默认的行为是控制退出程序;但如果你监听了此事件,你可以控制是否退出程序。 如果用户按下了 Cmd + Q,或者开发者调用了 app.quit(),Electron 会首先关闭所有的窗口然后触发 will-quit 事件,在这种情况下 window-all-closed事件不会被触发。

代码以及程序演示:

方法:

由于各种app.方法使用频率都差不多,那么我们根据需要看官方文档吧(也比较容易看懂)

[App.方法 文档]: “https://www.electronjs.org/zh/docs/latest/api/app#方法

BrowserWindow:

BrowserWindow 类暴露了各种方法来修改应用窗口的外观和行为

(1)优雅的打开窗口

ready-to-show 事件

在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show 事件 。 在此事件后显示窗口将没有视觉闪烁:(不会有白屏出现,但如果绘制没有完成,窗口出现的时间也会延长,根据业务需求来选择是否要选择这种优雅的打开窗口方式)

这个事件通常在 did-finish-load 事件之后发出,但是页面有许多远程资源时,它可能会在 did-finish-load之前发出事件。

backgroundColor属性

对于一个复杂的应用,ready-to-show 可能发出的太晚,会让应用感觉缓慢。 在这种情况下,建议立刻显示窗口,并使用接近应用程序背景的 backgroundColor

建议 设置 backgroundColorready-to-show 事件一起用,以使应用感觉更接近原生。

//backgroundColor有效值
const win = new BrowserWindow()
win.setBackgroundColor('hsl(230, 100%, 50%)')
win.setBackgroundColor('rgb(255, 145, 145)')
win.setBackgroundColor('#ff00a3')
win.setBackgroundColor('blueviolet')
(2)父子窗口&&模态窗口

父子窗口:

通过使用 parent 选项,你可以创建子窗口:

const { BrowserWindow } = require('electron')

const top = new BrowserWindow()
const child = new BrowserWindow({ parent: top })
child.show()
top.show()

child 窗口将总是显示在 top 窗口的顶部.

模态窗口

模态窗口是禁用父窗口的子窗口,创建模态窗口必须设置 parentmodal 选项:

const { BrowserWindow } = require('electron')

const child = new BrowserWindow({ parent: top, modal: true, show: false })
child.loadURL('https://github.com')
child.once('ready-to-show', () => {
  child.show()
})

(3)BrowserWindow属性

这个BrowserWindow属性 比较多,而且都是设置窗口的一些属性,也是不难理解,我们就对照我们的官方文档来学习吧!

[BrowserWindow属性]: “https://www.electronjs.org/zh/docs/latest/api/browser-window#new-browserwindowoptions

(4)webContents

did-finish-load 事件

导航完成时触发,即选项卡的旋转器将停止旋转,并指派onload事件后。

[webContents类实例事件]: “https://www.electronjs.org/zh/docs/latest/api/web-contents#类-webcontents

(5)保存窗口状态

当我们使用桌面程序的时候,那么我们有时候希望我们关闭程序再次打开程序的时候,程序的界面和状态能够是我们关闭前的样子,不希望再去重新设置。那我们就需要一个包electron-win-state

安装这个包

npm i electron-win-state -S

我们可以在npmjs官网上找到这个包,去看它的说明文档

根据使用说明,我们代码如下:

我们发现报错了,WinState is not a constructor

当我们打印一下WinState看看什么情况

const WinState = require('electron-win-state')
console.log(WinState)

发现没有直接引入WinState,我们修改一下

const WinState = require('electron-win-state').default 

现在就没报错了。

但是我们发现,虽然安装了这个包,但是我们程序的状态还是没有改变,这就很奇怪了,其实,是我们自己已经给窗口设置了自定义宽高,覆盖了。我们也缺了一句代码;

 winState.manage(win)

现在我们的状态保存就没问题了。当我们关闭窗口再次打开的时候就是关闭之前的样子了。

2.dialog

这些具体参数请看官方文档,下面几个方法返回一个promise对象

(1)dialog.showOpenDialog([browserWindow, ]options)
(2)dialog.showSaveDialog([browserWindow, ]options)
(3)dialog.showMessageBox([browserWindow, ]options)

[dialog文档]: “https://www.electronjs.org/zh/docs/latest/api/dialog#dialogshowopendialogsyncbrowserwindow-options

3.快捷键(globalShortcut)

在应用程序没有键盘焦点时,监听键盘事件。

globalShortcut 模块可以在操作系统中注册/注销全局快捷键, 以便可以为操作定制各种快捷键。

注意: 快捷方式是全局的; 即使应用程序没有键盘焦点, 它也仍然在持续监听键盘事件。 在 app 模块的 ready 事件就绪之前,这个模块不能使用。

const { app, globalShortcut } = require('electron')

app.whenReady().then(() => {
  // 注册一个'CommandOrControl+X' 快捷键监听器
  const ret = globalShortcut.register('CommandOrControl+X', () => {
    console.log('CommandOrControl+X is pressed')
  })

  if (!ret) {
    console.log('registration failed')
  }

  // 检查快捷键是否注册成功
  console.log(globalShortcut.isRegistered('CommandOrControl+X'))
})

app.on('will-quit', () => {
  // 注销快捷键
  globalShortcut.unregister('CommandOrControl+X')

  // 注销所有快捷键
  globalShortcut.unregisterAll()
})

方法

globalShortcut 模块具有以下方法:

(1)globalShortcut.register(accelerator, callback)
  • accelerator Accelerator
  • callback Function

返回boolean - 快捷键注册是否成功

注册 accelerator 的全局快捷键。 当用户按下注册快捷键时, callback 会被调用。

如果指定的快捷键已经被其他应用程序注册掉, 调用会默默失败。 该特性由操作系统定义,因为操作系统不希望多个程序的全局快捷键互相冲突。

在 macOS 10.14 Mojave 下面,如果 app 没有被授权为可信任使用的客户端,那么下列快捷键会注册失败:

  • “Media Play/Pause”
  • “Media Next Track”
  • “Media Previous Track”
  • “Media Stop”
(2)globalShortcut.registerAll(accelerators, callback)
  • accelerators string[] - an array of Accelerators.
  • callback Function

注册多个全局快捷键。 当用户按下注册快捷键时, callback 会被调用。

如果定义的快捷键已经被其他应用占用,这个调用会失效。 该特性由操作系统定义,因为操作系统不希望多个程序的全局快捷键互相冲突。

在 macOS 10.14 Mojave 下面,如果 app 没有被授权为可信任使用的客户端,那么下列快捷键会注册失败:

  • “Media Play/Pause”
  • “Media Next Track”
  • “Media Previous Track”
  • “Media Stop”
(3)globalShortcut.isRegistered(accelerator)
  • accelerator Accelerator

Returns boolean - 表示 accelerator 全局快捷键是否注册成功

当快捷键已经被其他应用程序注册时, 此调用依然将返回 false。 该特性由操作系统定义,因为操作系统不希望多个程序的全局快捷键互相冲突。

(4)globalShortcut.unregister(accelerator)
  • accelerator Accelerator

注销 accelerator 的全局快捷键。

(5)globalShortcut.unregisterAll()

注销所有的全局快捷键(清空该应用程序的所有全局快捷键)。

4.菜单(Menu)

我们这举个例子:

//一个简单的菜单模板
const { app, Menu } = require('electron')

const isMac = process.platform === 'darwin'//判断是不是macOS
//role:菜单选项
const template = [
  // { role: 'appMenu' }
  ...(isMac ? [{
    label: app.name,//我们程序名称
    submenu: [
      { role: 'about' },
      { type: 'separator' },//分割线
      { role: 'services' },
      { type: 'separator' },//分割线
      { role: 'hide' },
      { role: 'hideOthers' },
      { role: 'unhide' },
      { type: 'separator' },//分割线
      { role: 'quit' }
    ]
  }] : []),
  // { role: 'fileMenu' }
  {
    label: 'File',
    submenu: [
      isMac ? { role: 'close' } : { role: 'quit' }
    ]
  },
  // { role: 'editMenu' }
  {
    label: 'Edit',
    submenu: [
      { role: 'undo' },
      { role: 'redo' },
      { type: 'separator' },//分割线
      { role: 'cut' },
      { role: 'copy' },
      { role: 'paste' },
      ...(isMac ? [
        { role: 'pasteAndMatchStyle' },
        { role: 'delete' },
        { role: 'selectAll' },
        { type: 'separator' },//分割线
        {
          label: 'Speech',
          submenu: [
            { role: 'startSpeaking' },
            { role: 'stopSpeaking' }
          ]
        }
      ] : [
        { role: 'delete' },
        { type: 'separator' },//分割线
        { role: 'selectAll' }
      ])
    ]
  },
  // { role: 'viewMenu' }
  {
    label: 'View',
    submenu: [
      { role: 'reload' },
      { role: 'forceReload' },
      { role: 'toggleDevTools' },
      { type: 'separator' },//分割线
      { role: 'resetZoom' },
      { role: 'zoomIn' },
      { role: 'zoomOut' },
      { type: 'separator' },//分割线
      { role: 'togglefullscreen' }
    ]
  },
  // { role: 'windowMenu' }
  {
    label: 'Window',
    submenu: [
      { role: 'minimize' },
      { role: 'zoom' },
      ...(isMac ? [
        { type: 'separator' },//分割线
        { role: 'front' },
        { type: 'separator' },//分割线
        { role: 'window' }
      ] : [
        { role: 'close' }
      ])
    ]
  },
  {
    role: 'help',
    submenu: [
      {
        label: 'Learn More',
        click: async () => {
          //定义自己的菜单方法
          const { shell } = require('electron')
          await shell.openExternal('https://electronjs.org')
        }
      },
      {
        label:'alanmf',
          click:()=>{console.log('shift+G')},
          accelerator:'shift+G'//定义快捷键
      }
    ]
  }
]

const menu = Menu.buildFromTemplate(template)//一般来说, template是一个options类型的数组,用于构建MenuItem。 使用方法可参考前文。还可以将其他字段附加到template,它们将成为菜单项的属性。
Menu.setApplicationMenu(menu)//在macOS上将 menu设置成应用内菜单 在windows和Linux上,menu 将会被设置成窗口顶部菜单在Windows和Linux中

[Menu菜单文档]: “https://www.electronjs.org/zh/docs/latest/api/menu
[role的属性值]: “https://www.electronjs.org/zh/docs/latest/api/menu-item#角色

5.系统托盘(Tray)

添加图标和上下文菜单到系统通知区

const { app, Menu, Tray } = require('electron')

let tray = null
app.whenReady().then(() => {
  tray = new Tray('/path/to/my/icon') //定义icon的路径
  const contextMenu = Menu.buildFromTemplate([
    { label: 'Item1', type: 'radio' },
    { label: 'Item2', type: 'radio' },
    { label: 'Item3', type: 'radio', checked: true },
    { label: 'Item4', type: 'radio' }
  ])
  tray.setToolTip('This is my application.')//设置鼠标指针在托盘图标上悬停时显示的文本
  tray.setContextMenu(contextMenu)//设置上下文菜单到托盘
})

其实这个Tray跟Menu是布局差不太多的,所以学习起来也不是很难理解了。具体事件和方法也不是特别多,我们就直接看官方API文档吧!

[Tray文档]: “https://www.electronjs.org/zh/docs/latest/api/tray

6.触控板(TouchBar)

注意: TouchBar API目前为实验性质,可能会更改或删除。

如果官网推荐的TouchBar模拟器下载失败,自己搜索一下即可。(如果下载的打不开,那就在应用程序->右键显示包内容->如下)执行就可以了

//touchbar.js
const { app, BrowserWindow, TouchBar } = require('electron')

const { TouchBarLabel, TouchBarButton, TouchBarSpacer } = TouchBar

let spinning = false

// 类似老虎机的滚轴标签
const reel1 = new TouchBarLabel()
const reel2 = new TouchBarLabel()
const reel3 = new TouchBarLabel()

// 旋转结果标签
const result = new TouchBarLabel()

// 旋转按钮
const spin = new TouchBarButton({
  label: '🎰 Spin',
  backgroundColor: '#7851A9',
  click: () => {
    // Ignore clicks if already spinning
    if (spinning) {
      return
    }

    spinning = true
    result.label = ''

    let timeout = 10
    const spinLength = 4 * 1000 // 4 seconds
    const startTime = Date.now()

    const spinReels = () => {
      updateReels()

      if ((Date.now() - startTime) >= spinLength) {
        finishSpin()
      } else {
        // Slow down a bit on each spin
        timeout *= 1.1
        setTimeout(spinReels, timeout)
      }
    }

    spinReels()
  }
})

const getRandomValue = () => {
  const values = ['🍒', '💎', '7️⃣', '🍊', '🔔', '⭐', '🍇', '🍀']
  return values[Math.floor(Math.random() * values.length)]
}

const updateReels = () => {
  reel1.label = getRandomValue()
  reel2.label = getRandomValue()
  reel3.label = getRandomValue()
}

const finishSpin = () => {
  const uniqueValues = new Set([reel1.label, reel2.label, reel3.label]).size
  if (uniqueValues === 1) {
    // All 3 values are the same
    result.label = '💰 Jackpot!'
    result.textColor = '#FDFF00'
  } else if (uniqueValues === 2) {
    // 2个值相同
    result.label = '😍 Winner!'
    result.textColor = '#FDFF00'
  } else {
    // 没有值相同
    result.label = '🙁 Spin Again'
    result.textColor = null
  }
  spinning = false
}

const touchBar = new TouchBar({
  items: [
    spin,
    new TouchBarSpacer({ size: 'large' }),
    reel1,
    new TouchBarSpacer({ size: 'small' }),
    reel2,
    new TouchBarSpacer({ size: 'small' }),
    reel3,
    new TouchBarSpacer({ size: 'large' }),
    result
  ]
})

let window

app.whenReady().then(() => {
  window = new BrowserWindow({
    frame: false,
    titleBarStyle: 'hiddenInset',
    width: 200,
    height: 200,
    backgroundColor: '#000'
  })
  window.loadURL('about:blank')
  window.setTouchBar(touchBar)
})

运行命令:

./node_modules/.bin/electron touchbar.js

[TouchBar文档]: “https://www.electronjs.org/zh/docs/latest/api/touch-bar"

参考链接:

[Electron官方文档]: “https://www.electronjs.org/zh/docs/latest/api/app
[npmjs]: “https://www.npmjs.com/

Electron-d-1.0

Electron

一、构建第一个electron应用

1.初始化

npm init -y //初始化项目
npm install --save-dev electron //下载electron到开发依赖

2.创建入口index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using Node.js <span id="node-version"></span>,
    Chromium <span id="chrome-version"></span>,
    and Electron <span id="electron-version"></span>.
  </body>
</html>

3.创建入口文件

//main.js
const { app,BrowserWindow } = require('electron')

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600
    })
  
    win.loadFile('index.html')
  }

app.whenReady().then(() => {
    createWindow()
  })

4.创建窗口应用

Package.json配置

//Package.json
{
  "name": "electron-app",
  "version": "1.0.0",
  "description": "一个处于实验阶段的app",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "keywords": [],
  "author": "alanmf",
  "license": "MIT",
  "devDependencies": {
    "electron": "^21.3.0"
  }
}
npm start //执行 electron .

为了更方便页面更新我们使用nodemon

nodemon是一个自动重启node应用的工具,当监听的文件或监听目录下的文件发生修改时,自动重启应用

npm i nodemon -D

更改package.json

//Package.json
{
  "name": "electron-app",
  "version": "1.0.0",
  "description": "一个处于实验阶段的app",
  "main": "main.js",
  "scripts": {
    "start": "nodemon --exec electron ."
  },
  "keywords": [],
  "author": "alanmf",
  "license": "MIT",
  "devDependencies": {
    "electron": "^21.3.0"
  }
}

二、electron核心

1.electron核心概念

主进程;启动项目时运行的main.js脚本就是我们说的主进程,在主进程运行的脚步可以创建web页面的形式展示GUI,主进程只有一个。

渲染进程:每个Electron的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。

主进程使用BrowserWindow创建实例,主进程销毁后,对应的渲染进程会被终止。主进程与渲染进程通过 IPC 的方式(事件驱动)进行通信。

2.electron窗口控制台

//打开开发者工具
win.webContents.openDevTools();
//暂时关闭安全警告(不推荐)
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
//配置CSP(阻挡一部分安全警告)
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

3.主进程事件生命周期

关闭所有窗口时退出应用 Windows & Linux

在Windows和Linux上,关闭所有窗口通常会完全退出一个应用程序。

为了实现这一点,你需要监听 app 模块的 'window-all-closed'事件。如果用户不是在 macOS(darwin) 上运行程序,则调用 app.quit()`。

app.on('window-all-closed', () => {
  //macOS用户在关闭窗口时不会关闭应用
  if (process.platform !== 'darwin') app.quit()
})

当 Linux 和 Windows 应用在没有窗口打开时退出了,macOS 应用通常即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。

为了实现这一特性,监听 app 模块的 activate 事件。如果没有任何浏览器窗口是打开的,则调用 createWindow() 方法。

因为窗口无法在 ready 事件前创建,你应当在你的应用初始化后仅监听 activate 事件。 通过在您现有的 whenReady() 回调中附上您的事件监听器来完成这个操作。

app.whenReady().then(() => {
  createWindow()
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

这样,我们的窗口的控件就完整了(实现macOS用户-没有窗口打开时创建新窗口-关闭窗口不关闭应用,点击程序坞应用打开新窗口)

4.通过预加载脚本从渲染器访问Node.js

现在,最后要做的是输出Electron的版本号和它的依赖项到你的web页面上。

在主进程通过Node的全局 process 对象访问这个信息是微不足道的。 然而,你不能直接在主进程中编辑DOM,因为它无法访问渲染器 文档 上下文。 它们存在于完全不同的进程!

这是将 预加载 脚本连接到渲染器时派上用场的地方。 预加载脚本在渲染器进程加载之前加载,并有权访问两个 渲染器全局 (例如 windowdocument) 和 Node.js 环境。

创建一个名为 preload.js 的新脚本:

//preload.js
window.addEventListener('DOMContentLoaded', () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector)
    if (element) element.innerText = text
  }

  for (const dependency of ['chrome', 'node', 'electron']) {
    replaceText(`${dependency}-version`, process.versions[dependency])
  }
})

上面的代码访问 Node.js process.versions 对象,并运行一个基本的 replaceText 辅助函数将版本号插入到 HTML 文档中。

要将此脚本附加到渲染器流程,请在你现有的 BrowserWindow 构造器中将路径中的预加载脚本传入 webPreferences.preload 选项。

// include the Node.js 'path' module at the top of your file
const path = require('path')

// modify your existing createWindow() function
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  win.loadFile('index.html')
}
// ...

这里使用了两个Node.js概念:

  • __dirname字符串指向当前正在执行脚本的路径 (在本例中,它指向你的项目的根文件夹)。
  • path.join API 将多个路径联结在一起,创建一个跨平台的路径字符串。

我们使用一个相对当前正在执行JavaScript文件的路径,这样相对路径将在开发模式和打包模式中都将有效。

5.contextBridge

contextBridge模块有以下方法:

contextBridge.exposeInMainWorld(apiKey, api)

  • apiKey string - 将 API 注入到 窗口 的键。 API 将可通过 window[apiKey] 访问。
  • api any - 你的 API可以是什么样的以及它是如何工作的相关信息如下。

应用开发接口(API)

提供给 exposeInMainWorldapi 必须是一个 FunctionstringnumberArrayboolean;或一个键为字符串,值为一个 FunctionstringnumberArrayboolean的对象;或其他符合相同条件的嵌套对象。

Function 类型的值被代理到其他上下文中,所有其他类型的值都会被 复制冻结。 在 API 中发送的任何数据 /原始数据将不可改变,在桥接器其中一侧的更新不会导致另一侧的更新。

代码应用:

//preload.js+
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myApi',{
    platform:process.platform
  })

那我们就可以在页面上去访问window上的方法了

//app.js
console.log(window.myApi.platform) //darwin

三、主进程与渲染进程通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

在 Electron 中,进程使用 ipcMainipcRenderer模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。

1.渲染器进程到主进程(单向 )

要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.sendAPI 发送消息,然后使用 ipcMain.onAPI 接收。

我们下边就按照官网的例子来说吧!

(1) 使用 ipcMain.on 监听事件

在主进程(main.js)中,使用 ipcMain.on API 在 set-title 通道上设置一个 IPC 监听器:

//main.js+
const { ipcMain } = require('electron')
 ipcMain.on('set-title', (event, title) => {
      const webContents = event.sender
      const win = BrowserWindow.fromWebContents(webContents)
      win.setTitle(title)
    })
(2) 通过预加载脚本暴露 ipcRenderer.send

要将消息发送到上面创建的 IPC 监听器,可以使用 ipcRenderer.send API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,需要使用 contextBridge API 来选择要从预加载脚本中暴露哪些 API。

在您的预加载脚本中添加以下代码,向渲染器进程暴露一个全局的 window.electronAPI 变量。

//preload.js+
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

此时,将能够在渲染器进程中使用 window.electronAPI.setTitle() 函数。

(3) 构建渲染器进程 UI

在 BrowserWindow 加载的我们的 HTML 文件中,添加一个由文本输入框和按钮组成的基本用户界面:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    Title: <input id="title"/>
    <button id="btn" type="button">Set</button>
    <script src="./renderer/app.js"></script>
  </body>
</html>

将在导入的 renderer.js 文件中添加几行代码,以利用从预加载脚本中暴露的 window.electronAPI 功能:

//app.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
    const title = titleInput.value
    window.electronAPI.setTitle(title)
});

到目前的代码为止,我们的一个渲染进程到主进程的进程通信就已经实现了,我们可以试一试我们的UI界面功能

总结:

!!!我们上面有一些地方不理解的方法和API,我们现在讲一下

上面的 回调函数有两个参数:一个 IpcMainEvent 结构和一个 title 字符串。 每当消息通过 set-title 通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle API。

我们只是为了更简单的理解进程通信,具体 API 见官方文档吧!

2.渲染器进程到主进程(双向)

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invokeipcMain.handle 搭配使用来完成。

(1)使用 ipcMain.handle 监听事件

在主进程(main.js)中,我们将创建一个 handleFileOpen() 函数,它调用 dialog.showOpenDialog 并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile 通道发送 ipcRender.invoke 消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke 调用。

//main.js
const { app,BrowserWindow,ipcMain,dialog } = require('electron')
const path = require('path')

//回调函数
async function handleFileOpen() {
  const { canceled, filePaths } = await dialog.showOpenDialog()
  if (canceled) {
    return
  } else {
    return filePaths[0]
  }
}

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        preload: path.join(__dirname, 'preload.js')
      }
    })
    win.loadFile('index.html') 
    win.webContents.openDevTools()
    
  }

  app.whenReady().then(() => {
    //监听,执行回调
    ipcMain.handle('dialog:openFile', handleFileOpen)
    createWindow()
    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  })
  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })
(2)通过预加载脚本暴露 ipcRenderer.invoke

在预加载脚本中,我们暴露了一个单行的 openFile 函数,它调用并返回 ipcRenderer.invoke('dialog:openFile') 的值。 我们将在下一步中使用此 API 从渲染器的用户界面调用原生对话框。

//preload.js
const { contextBridge,ipcRenderer } = require('electron')
 contextBridge.exposeInMainWorld('electronAPI', {
    openFile: () => ipcRenderer.invoke('dialog:openFile')
})
(3)构建渲染器进程 UI

最后,让我们构建加载到 BrowserWindow 中的 HTML 文件。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Dialog</title>
  </head>
  <body>
    <button type="button" id="btn">Open a File</button>
    File path: <strong id="filePath"></strong>
    <script src='./renderer/app.js'></script>
  </body>
</html>

用户界面包含一个 #btn 按钮元素,将用于触发我们的预加载 API,以及一个 #filePath 元素,将用于显示所选文件的路径。需要在渲染器进程脚本中编写几行代码

//app.js
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.openFile()
  filePathElement.innerText = filePath
})

到目前的代码为止,我们的一个渲染进程到主进程的双向进程通信就已经实现了,我们可以试一试我们的UI界面功能

总结:

ipcRenderer.invoke API (老方法了,但依然好用)是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。 但这种 IPC 模式存在几种替代方法。

使用 ipcRenderer.send我们用于单向通信的 ipcRenderer.send API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。

//preload.js
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-reply', (_event, arg) => {
  console.log(arg) // 在 DevTools 控制台中打印“pong”
})
ipcRenderer.send('asynchronous-message', 'ping')
//main.js
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // 在 Node 控制台中打印“ping”
  // 作用如同 `send`,但返回一个消息
  // 到发送原始消息的渲染器
  event.reply('asynchronous-reply', 'pong')
})

这种方法有几个缺点:

  • 您需要设置第二个 ipcRenderer.on 监听器来处理渲染器进程中的响应。 使用 invoke,您将获得作为 Promise 返回到原始 API 调用的响应值。
  • 没有显而易见的方法可以将 asynchronous-reply 消息与原始的 asynchronous-message消息配对。 如果您通过这些通道非常频繁地来回传递消息,则需要添加其他应用代码来单独跟踪每个调用和响应。

使用 ipcRenderer.sendSyncAPI也是可以替代的,但不推荐

ipcRenderer.sendSync API 向主进程发送消息,并 同步 等待响应。

//main.js
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // 在 Node 控制台中打印“ping”
  event.returnValue = 'pong'
})
//perload.js
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')

const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // 在 DevTools 控制台中打印“pong”

这份代码的结构与 invoke 模型非常相似,但出于性能原因,我们建议避免使用此 API。 它的同步特性意味着它将阻塞渲染器进程,直到收到回复为止。(容易阻塞渲染器进程)

3.主进程到渲染器进程

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents实例发送到渲染器进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

(1)使用 webContents 模块发送消息

我们需要首先使用 Electron 的 Menu 模块在主进程中构建一个自定义菜单,该模块使用 webContents.send API 将 IPC 消息从主进程发送到目标渲染器。

//main.js
const { app,BrowserWindow,ipcMain,Menu } = require('electron')
const path = require('path')

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        preload: path.join(__dirname, 'preload.js')
      }
    })

    const menu = Menu.buildFromTemplate([
      {
        label: app.name,
        submenu: [
        {
          click: () => win.webContents.send('update-counter', 1),
          label: 'Increment',
        },
        {
          click: () => win.webContents.send('update-counter', -1),
          label: 'Decrement',
        }
        ]
      }
  
    ])

    Menu.setApplicationMenu(menu)
    win.loadFile('index.html') 
    win.webContents.openDevTools()
    
  }

  app.whenReady().then(() => {
    ipcMain.on('counter-value', (_event, value) => {
      console.log(value) // will print value to Node console
    })
    createWindow()
    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  })

  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })
(2)通过预加载脚本暴露 ipcRenderer.on

与前面的渲染器到主进程的示例一样,我们使用预加载脚本中的 contextBridgeipcRenderer 模块向渲染器进程暴露 IPC 功能:

//preload.js
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
    handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})

加载预加载脚本后,渲染器进程应有权访问 window.electronAPI.handleCounter() 监听器函数。

(3)构建渲染器进程 UI

我们将在加载的 HTML 文件中创建一个接口,其中包含一个 #counter元素,我们将使用该元素来显示值:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Menu Counter</title>
  </head>
  <body>
    Current value: <strong id="counter">0</strong>
    <script src='./renderer/app.js'></script>
  </body>
</html>

为了更新 HTML 文档中的值,我们将添加几行 DOM 操作的代码,以便在每次触发 update-counter 事件时更新 #counter 元素的值。

//app.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
    const oldValue = Number(counter.innerText)
    const newValue = oldValue + value
    counter.innerText = newValue
    event.sender.send('counter-value', newValue)
})

在上面的代码中,我们将回调传递给从预加载脚本中暴露的 window.electronAPI.onUpdateCounter 函数。 第二个 value 参数对应于我们传入 webContents.send 函数的 1-1,该函数是从原生菜单调用的。

到目前的代码为止,我们的一个主进程到渲染器进程的进程通信就已经实现了,我们可以试一试我们的UI界面功能

总结:

对于从主进程到渲染器进程的 IPC,没有与 ipcRenderer.invoke 等效的 API。 不过,可以从 ipcRenderer.on 回调中将回复发送回主进程。

我们可以对前面例子的代码进行略微修改来演示这一点。 在渲染器进程中,使用 event 参数,通过 counter-value 通道将回复发送回主进程。

//app.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
    const oldValue = Number(counter.innerText)
    const newValue = oldValue + value
    counter.innerText = newValue
    event.sender.send('counter-value', newValue)
})

在主进程中,监听 counter-value 事件并适当地处理它们。

//main.js
ipcMain.on('counter-value', (_event, value) => {
      console.log(value) // will print value to Node console
  })

4.渲染器进程到渲染器进程

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,有两种选择:

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。

参考链接:

[Electron官网]: “https://www.electronjs.org/zh/docs/latest/tutorial/quick-start

  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信