基础入门

更新时间: 2024-12-13 14:53:22

# 前言

当今3D变得越来越流行,并且在视频游戏以外的领域也广泛使用,其中之一就是网站。

近几年来3D网页已经成为一种趋势,这主要归功于现代浏览器和移动设备都能支持3D。

如果你是希望制作含有3D的网页或app的前端开发人员,有些概念需要简单了解一下。

  • OpenGL
    OpenGL是一个应用程序编程接口(api),其作用是能够使应用程序与显卡交互,并在显示屏上渲染东西。

  • WebGL OpenGL以某种方式编写的应用程序连接着图形和显卡,但web浏览器并不能运行OpenGL。2011年3月,WebGL首次发布,它是轻量级的OpenGL,也被称作OpenGL ES,可以在浏览器上运行。
    webgl只是一些用来连接浏览器和显卡的JavaScript函数,这些函数可以在网页上显示2d和3d的内容。

  • Shader
    webgl是浏览器和显卡之间的媒介,那么shader就是我们给显卡发送的指令,它们本质上是被嵌入应用程序代码的一些小的程序。

有两种shader: 顶点着色器vertex shader 和 片元着色器fagment shader

顶点着色器的作用是简单的指定一个或多个组成网格的点的位置。

片元着色器负责每个像素点的颜色

  • 3D库
    threejs,babylonjs,以及其他库都充当着实际的程序和webgl api之间的中间层,大大的简化了代码复杂度。

这些库的api不重要,重要的是要知道api是怎样在幕后运行的

# 创建项目

  • 新建文件夹,在terminal中输入 npm create vite@latest

  • 给project命名

  • 使用vanilla js

  • 使用JavaScript

  • 打开刚刚新建的项目文件夹,npm install, npm run dev启动服务器

  • 安装babylonjs npm install -D @babylonjs/core

  • 删除掉不需要的文件并清空其他文件,src下只保留空的main.js和style.css,再次运行服务

# babylonjs必须的4个基本元素

  • 画布,是一个HTML容器
  • Engine,它是babylonjs的应用程序大脑,将用户输入的逻辑转换为生动的3D图形
  • Scene,场景是呈现3D的空间
  • Camera,相机的作用是在场景中显示某个空间

# 初次实践

  • 首先添加canvas元素

  • 在main.js文件中,导入babylonjs,并且给canvas分配一个变量

import * as BABYLON from '@babylonjs/core'

const canvas = document.getElementById('renderCanvas')
1
2
3
  • 接下来创建Babylonjs Engine的示例,并将画布传递过去
const engine = new BABYLON.Engine(canvas)
1
  • 创建createScene函数,这个函数将要实现99%的功能,在这个函数中,我们会返回一个场景实例,然后调用这个函数给实例分配一个变量
const createScene = function() {
  const scene = new BABYLON.Scene(engine)

  return scene
}

const scene = createScene()
1
2
3
4
5
6
7
  • 然后运行渲染循环
engine.runRenderLoop(function() {
  scene.render()
})
1
2
3
  • 让场景填充整个屏幕
html, body {
  overflow: hidden;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

#canvasRender {
  width: 100%;
  height: 100%;
  touch-action: none;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 快速添加一个摄像头和一个盒子



 
 




const createScene = function() {
  const scene = new BABYLON.Scene(engine)

  scene.createDefaultCameraOrLight(true, false, true)
  const box = new BABYLON.MeshBuilder.CreateBox()

  return scene
}
1
2
3
4
5
6
7
8

现在可以再场景中得到一个大盒子,但存在一个问题,如果我调整了窗口的大小,整个场景中的盒子形状就会变形

  • 监听window的resize事件,在回调函数中调用engine的resize方法
window.addEventListener('resize',function() {
  engine.resize()
})
1
2
3

现在window的大小发生变化,盒子的形状不会变了

场景中的对象通常叫做 Mesh,它可以是一个盒子,一个球体,一个复杂的形状,甚至更复杂的比如使用blender之类的软件创建的模型

Babylonjs提供了一组内置网格

我们可以通过向构造函数中传入参数来改变网格,这个参数在文档 (opens new window)中自己查看

const ground = new BABYLON.MeshBuilder.CreateGround('', {
    height: 10,
    width: 10,
    subdivisions: 30
  })

  ground.material = new BABYLON.StandardMaterial()
  ground.material.wireframe = true
1
2
3
4
5
6
7
8

这段代码可以创建一个分了30段的地面,并切用线框模式渲染

高度图是一张灰度图,越白的地方越高,越黑的地方越低,现在我们用高度图创建一个地形

 const groundFromHM = new BABYLON.MeshBuilder.CreateGroundFromHeightMap('','/Terrain_heightmap.png',{
    height:10,
    width: 10,
    subdivisions: 50
  })

  groundFromHM.material = new BABYLON.StandardMaterial()
  groundFromHM.material.wireframe = true 
1
2
3
4
5
6
7
8

# 添加文本

创建的文本原点在文本中心。

添加文本需要使用ExtrudePolygon功能,该功能需要有Earcut库。虽然Earcut脚本在Playground中已经预加载,但是在自己的项目中需要添加对该脚本的引用。

首先安装earcut
npm install -S earcut

然后在main.js中引用

import earcut from 'earcut'

window.earcut = earcut
1
2
3

还需要在facetype.js (opens new window)上将字体文件转换为json

 const fontData = await (await fetch("/FZZongYi-M05S_Regular.json")).json();
  const text = BABYLON.MeshBuilder.CreateText(
    "myText",
    "hello",
    fontData,
    {
      size: 16,
      resolution: 64,
      depth: 1,
    },
    scene
  );
1
2
3
4
5
6
7
8
9
10
11
12

属性 默认值
size 每个字母的大小 50
rosolution 每根曲线使用的点数 8
depth 沿Y轴挤出的深度 1.0
sideOrientation 面方向 DOUBLESIDE

# 相机

babylonjs有多种相机,但是作为初学者一般只会用两种相机:

  • universal camera
    通用相机,通常用于第一人称射击游戏,可以使用鼠标选择方向,然后使用方向键移动
  • arc rotate camrea
    圆弧旋转相机,由旋转角度,目标和半径定义

添加默认灯光,然后添加Universal Camera,它有三个参数,第一个是名字,第二个是初始位置,第三个是scene, 然后允许用户控制

scene.createDefaultLight()
const camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(0, 5, -10), scene)
camera.attachControl(true)
1
2
3


可以看到现在我们可以通过箭头来控制前进后退,我们可以用鼠标做到相同的事,只需加上一行代码

  camera.inputs.addMouseWheel()
1

我们也可以给相机一个初始的观察对象

  camera.setTarget(BABYLON.Vector3.Zero())
1

接下来我们创建arc rotate camera,构造函数有6个参数,name,α,β ,radius,target,scene
α是绕y轴的旋转,β是绕z轴的旋转

  const camera = new BABYLON.ArcRotateCamera('camera',0,0,10,new BABYLON.Vector3(0,0,0),scene)
  camera.attachControl(true)  
  // 设置相机初始位置  
  camera.setPosition(new BABYLON.Vector3(0,0,-20))
  // 限制β旋转 
  camera.lowerBetaLimit = Math.PI / 4
  camera.upperBetaLimit = Math.PI / 2
  // 限制半径  
  camera.lowerRadiusLimit = 20
  camera.upperRadiusLimit = 50  
1
2
3
4
5
6
7
8
9
10

# Mesh

一个Mesh由两部分组成,一组表示其形状的点和线,还有覆盖这些线和点的材质
让我们来改变一个小球的颜色

  const sphere = new BABYLON.MeshBuilder.CreateSphere('mySphere', {
    segements: 50,
    diameter : 0.3,
    diameterY: 0.4
  }, scene)

  const sphereMaterial = new BABYLON.StandardMaterial()
  sphere.material = sphereMaterial

  // 改变小球颜色  
  sphereMaterial.diffuseColor = new BABYLON.Color3(0, 1, 0)
  // 改变高光颜色
  sphereMaterial.specularColor = new BABYLON.Color3(1, 0, 0)
  // 自发光颜色  
  sphereMaterial.emissiveColor = new BABYLON.Color3(0, 0, 1)
  // 环境光颜色,需要设置scene的环境光
  sphereMaterial.ambientColor = new BABYLON.Color3(0, 1, 1)
  scene.ambientColor = new BABYLON.Color3(0, 1, 0.5)
  // 透明度
  sphereMaterial.alpha = 0.2
  // 线框模式
  sphereMaterial.wireframe = true  
  // 漫反射贴图  
  sphereMaterial.diffuseTexture = new BABYLON.Texture('/wood_texture.jpg')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


diffuseColor和specularColor都会受光线影响,如果完全没有光线就是黑色,有光线就遵循光的三原色的叠加

接下来给地板贴图

 const ground = new BABYLON.MeshBuilder.CreateGround('',{
    height: 5,
    width:5,
    subdivisions:5,
    subdivisionsX: 10
  })

  const groundCatMat = new BABYLON.StandardMaterial()
  ground.material = groundCatMat
  groundCatMat.diffuseTexture = new BABYLON.Texture('/bg.jpg')

  // 水平位移
  groundCatMat.diffuseTexture.uOffset = 1.4  
  // 垂直位移
  groundCatMat.diffuseTexture.vOffset = 1.4  

  // 水平缩放
  groundCatMat.diffuseTexture.uScale = 5
  // 垂直缩放  
  groundCatMat.diffuseTexture.vScale = 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// 改变位置
box.position.x = 1;
box.position = new BABYLON.Vector3(-1, 0.5, 0)

// 改变旋转
box.rotation = Math.PI / 4
box.rotation = new BABYLON.Vector3(0, 0, Math.PI / 6)

// 改变缩放
box.scaling.y = 2
box.scaling = new BABYLON.Vector3(2, 0.5, 1)
1
2
3
4
5
6
7
8
9
10
11

# 调试Gizmo

Gizmo是可以附加到节点的对象
Gizmo由UtilityLayerRenderer显示,不会破坏现有的场景状态,工具层与场景独立

  const utilLayer = new BABYLON.UtilityLayerRenderer(scene)

  const positionGizmo = new BABYLON.PositionGizmo(utilLayer)
  positionGizmo.attachedMesh = ground

  const rotationGizmo = new BABYLON.RotationGizmo(utilLayer)
  rotationGizmo.attachedMesh = ground

  const scaleGizmo = new BABYLON.ScaleGizmo(utilLayer)
  scaleGizmo.attachedMesh = ground
1
2
3
4
5
6
7
8
9
10

# 动画

在每一帧之前都执行,下面的代码相当于每秒将box旋转0.01弧度60次

scene.registerBeforeRender(funciton() {
  box.rotation.x += 0.01
})
1
2
3

还有另一种动画的方式

  BABYLON.Animation.CreateAndStartAnimation(
    'xScaleAnimation', // 名字
    ground,  // 动画对象
    'scaling.x', // 动画属性
    30, // 每秒多少帧
    120, // 总共多少帧
    0, // 起始值
    2, // 结束值
    BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT// 循环方式  
    new BABYLON.CircleEase // 缓动函数  
  )
1
2
3
4
5
6
7
8
9
10
11

下面是更好控制动画的一种方案

  const animation = new BABYLON.Animation(
    'yRatAnimation',
    'rotation.y',
    30,
    BABYLON.Animation.ANIMATIONTYPE_FLOAT,
    BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
  )

  const animationKeys = []

  animationKeys.push({
    frame: 0,
    value: 0
  })

  animationKeys.push({
    frame: 60,
    value: 2 * Math.PI 
  })

  animation.setKeys(animationKeys)

  ground.animations = []
  ground.animations.push(animation)

  scene.beginAnimation(ground, 0, 60, true)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# Light

  • PointLight 点光
const light = new BABYLON.PointLight(
  'pointLight',
  new BABYLON.Vector3(0, 1, 0),
  scene
)
1
2
3
4
5

Babylonjs为光源提供了一个小Gizmo,可以帮助直观地显示它们的位置

  const utilLayer = new BABYLON.UtilityLayerRenderer(scene)
  const lightGizmo = new BABYLON.LightGizmo(utilLayer)
  lightGizmo.light = light
1
2
3

  • SpotLight, 聚光灯
  const light = new BABYLON.SpotLight(
    'pointLight', // 名字
    new BABYLON.Vector3(0, 1, 0), // 位置
    new BABYLON.Vector3(0, -1, 0), // 方向
    Math.PI / 3, // 角度
    2, // 衰减
    scene // 场景 
  )
1
2
3
4
5
6
7
8

  • directionalLight 平行光
const light = new BABYLON.DirectionalLight(
  'directionalLight',
  new BABYLON.Vector3(-2, -3, 0),
  scene
)
// 改变光强度  
light.intensity =  0.5  
1
2
3
4
5
6
7
  • hemispheric light 半球光
const light = new BABYLON.HemisphereicLight(
  'hemisohericLight',
  new BABYLON.Vector3(-5, -5, 0),
  scene
)
light.groundColor = new BABYLON.Color3(0, 1, 0)
light.diffuse = new BABYLON.Color3(0, 0, 1)
light.specular = new BABYLON.Color3(0, 1, 0)
1
2
3
4
5
6
7
8
  • 投影
  const ground = new BABYLON.MeshBuilder.CreateGround('',{
    height: 5,
    width:5,
    subdivisions:5,
    subdivisionsX: 10
  })

  const sphere = new BABYLON.MeshBuilder.CreateSphere('mySphere', {
    segments: 50,
    diameter: 0.3
  })
  sphere.position = new BABYLON.Vector3(1, 1.5, 0)  

  const light = new BABYLON.DirectionalLight(
    'directionalLight',
    new BABYLON.Vector3(-2, -3, 0),
    scene
  )

  const shadowGenerator = new BABYLON.ShadowGenerator(1024, light)
  shadowGenerator.addShadowCaster(sphere)
  ground.receiveShadows = true  

  shadowGenerator.setDarkness(0.5)
  shadowGenerator.useBlurCloseExponentialShadowMap = true
  shadowGenerator.useKernelBlur = true
  shadowGenerator.blurKernel = 64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Fog

  • 无雾
  • 线性模式
  scene.fogMode = BABYLON.Scene.FOGMODE_LINEAR
  scene.fogStart = 10
  scene.fogEnd = 60  
1
2
3
  • 指数模式
  scene.fogMode = BABYLON.Scene.FOGMODE_EXP2
  scene.fogDensity =  0.2 // 雾的浓度  
  scene.fogColor = new BABYLON.Color3(0, 1, 1)
1
2
3

# 光标选择

  scene.onPointerDown = function castRay() {
    const hit = scene.pick(scene.pointerX, scene.pointerY)

    if(hit.pickedMesh && hit.pickedMesh.name == 'mySphere') {
        hit.pickedMesh.material = new BABYLON.StandardMaterial()
        hit.pickedMesh.material.diffuseColor = BABYLON.Color3.Red()
    }
  }
1
2
3
4
5
6
7
8

# 加载模型

babylonjs可以加载不同格式的模型

  • .babylon
  • .obj
  • .glb

首先安装依赖
npm install -D @babylonjs/loaders

然后导入刚刚安装的模块

import '@babylon/loaders/glTF'  

BABYLON.SceneLoader.ImportMesh(
  '',
  '/',
  'Cow.gltf',
  scene,
  function(meshes, particalSystems, skeletons, animationGroups) {
    const model = meshes[0]  
    model.scaling = new BABYLON.Vector3(0.25, 0.25, 0.25)

    animationGroups[5].play(true)
  }
)

BABYLON.ImportMeshAsync("/ground1.glb", scene).then(() => {  
  const importedAnimGroups = result.animationGroups  
  importedAnimGroups[0].play(true)
}, (e) => {
  console.log("oh no! something is off", e);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 播放音乐

const bgMusic = new BABYLON.Sound('mySound', '/DivKid.mp3', scene, null, {
  loop:true,
  autoplay: true
})
1
2
3
4

# imspector检查器

这是babylonjs中最好的功能之一,首先我们需要安装检查器模块,
npm install -D @babylonjs/inspector

然后引入Inspector

import {Inspector} from '@babylonjs/inspector'
1

然后在代码的最末尾加上

Inspector.Show(scene, {})
1

babylon的魅力不仅仅在于代码本身,还有它提供的工具,比如说Playground,SandBox