1、需要会的知识点
three.js的场景,摄像机,渲染器,轨道控制器,坐标轴,场景适配,渲染循环
创建立方缓冲几何体、纹理、3d物体
实现:创建立方几何体,纹理贴图镜面反向渲染,摄像机设置在内部,通过与创建3D 标记物体交互事件切换纹理贴图,创建视频纹理实现展厅视频展示
架构:vite + js
预览图
一、初始化 vite 项目
命令:npm create vite@latest
选择 Vanilla 模版
选择 JS 语法
清空 main.js 和 style.css 内容
新建如下untils/init.js文件填入内容
初始化场景,摄像机,渲染器,轨道控制器,坐标轴,场景适配,渲染循环
// 目标:初始化 three.js 基础环境
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { CSS3DRenderer } from 'three/addons/renderers/CSS3DRenderer.js';
export let scene, camera, renderer, controls, css3dRenderer;
(function init() {
scene = new THREE.Scene()
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.z = 0.1
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
})();
(function createControls() {
controls = new OrbitControls(camera, renderer.domElement)
controls.minPolarAngle = 0.25 * Math.PI
controls.enableZoom = false
})();
(function createHelper() {
// const axesHelper = new THREE.AxesHelper(5)
// scene.add(axesHelper)
})();
(function resizeRender() {
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight)
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
})
})();
(function create3dRenderer(){
css3dRenderer = new CSS3DRenderer()
css3dRenderer.setSize(window.innerWidth, window.innerHeight)
css3dRenderer.domElement.style.position = 'fixed'
css3dRenderer.domElement.style.left = '0'
css3dRenderer.domElement.style.top = '0'
css3dRenderer.domElement.style.pointerEvents = 'none'
document.body.appendChild(css3dRenderer.domElement)
})();
(function renderLoop() {
renderer.render(scene, camera)
controls.update()
css3dRenderer.render(scene, camera)
requestAnimationFrame(renderLoop)
})();
main.js - 创建立方缓冲几何体
import { camera, scene } from './utils/init.js'
import * as THREE from 'three'
function createCube() {
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide })
const cube = new THREE.Mesh(geometry, material)
cube.scale.set(1, 1, -1)
scene.add(cube)
return cube
}
二、展厅-第一个页面内容展示
目标准备:
1.6 个面纹理图片(镜面翻转)
2.地上热点交互标记(借助 gui 定位位置)
const sceneInfoObj = {
one: { // 第一个场景里数据
publicPath: 'technology/1/',
imgUrlArr: ['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
markList: [ // 当前空间中所有标记信息对象
{
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.05, 0.05],
position: [-0.46, -0.11, -0.11],
rotation: [1.42, 0.68, 1.63],
targetAttr: 'two'
}
]
},
}
function setMaterialCube(infoObj) {
const { publicPath, imgUrlArr, markList } = infoObj
const textureLoader = new THREE.TextureLoader()
textureLoader.setPath(publicPath)
const materialArr = imgUrlArr.map(imgStr => {
const texture = textureLoader.load(imgStr)
texture.colorSpace = THREE.SRGBColorSpace
return new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide
})
})
cubeObj.material = materialArr
markList.forEach(markObj => {
// 地板标记
if (markObj.name === 'landMark') createLandMark(markObj)
})
scene.add(group)
}
function createLandMark(infoObj) {
const { imgUrl, wh, position, rotation, targetAttr } = infoObj
const geometry = new THREE.PlaneGeometry(...wh)
const material = new THREE.MeshBasicMaterial({
map: (new THREE.TextureLoader()).load(imgUrl),
side: THREE.DoubleSide,
transparent: true
})
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(...position)
mesh.rotation.set(...rotation)
// 给地上热点标记添加名字-方便点击时进行区分
mesh.name = 'mark'
// three.js 3D 物体也可以自定义属性和值(方便后续获取绑定的这个数据)
// 绑定这个地上热点标记,要切换到哪个场景信息对象,对应名字属性
mesh.userData.attr = targetAttr
group.add(mesh)
}
setMaterialCube(sceneInfoObj.one) // 默认先渲染第一个场景信息
准备
1.准备第二个场景相关数据
2.与 3D 物体交互事件绑定
3.准备清除当前场景热点标记函数
4.切换纹理,重新创建当下场景热点标记
two: {
publicPath: 'technology/2/',
imgUrlArr: ['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
markList: [
{
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.05, 0.05],
position: [0.47, -0.2, 0],
rotation: [1.48, 0.26, -1.78],
targetAttr: 'one' // 目标场景信息对象属性
}, {
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.05, 0.05],
position: [-0.46, -0.16, -0.3],
rotation: [1.21, 0.78, 0],
targetAttr: 'three' // 目标场景信息对象属性
}
]
},
function clear() {
// 清除组内物体
const list = [...group.children]
list.forEach(obj => {
if (!obj.isCSS3DObject) {
obj.geometry.dispose()
obj.material.dispose()
}
group.remove(obj)
})
}
// 在 setMaterialCube 里先调用 clear 清除当下场景空间中的物体标记(都在 Group 组中)
function bindClick() {
const rayCaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
// 5.2 与 3D 物体交互事件绑定
window.addEventListener('click', e => {
pointer.x = (e.clientX / window.innerWidth) * 2 - 1
pointer.y = -(e.clientY / window.innerHeight) * 2 + 1
rayCaster.setFromCamera(pointer, camera)
const list = rayCaster.intersectObjects(scene.children)
// 查找到我点击的热点标记物体
const obj = list.find(obj => obj.object.name === 'mark')
if (obj) {
// 提取物体上绑定的自定义属性,切换场景
// 5.4 切换纹理,重新创建当前场景下的热点标记
const infoObj = sceneInfoObj[obj.object.userData.attr]
setMaterialCube(infoObj)
}
})
}
四、展厅 第三个页面
1.准备第三个场景相关数据
2.与 3D 物体交互事件绑定
3.准备清除当前场景热点标记函数
4.切换纹理,重新创建当下场景热点标记
ps:因为前面流程代码已经准备好了,切换点击->关联场景属性 key 名 -> 清空当下空间物体 -> 重新创建新空间物体和纹理贴图,所以准备好数据就可以切换空间了
three: {
publicPath: 'technology/3/',
imgUrlArr: ['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
markList: [
{
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.05, 0.05],
position: [0.4, -0.18, 0.32],
rotation: [-1.53, -0.04, -1.26],
targetAttr: 'two' // 目标场景信息对象属性
}, {
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.05, 0.05],
position: [0.32, -0.16, -0.33],
rotation: [1.46, 0.1, -0.17],
targetAttr: 'four' // 目标场景信息对象属性
}
]
},
准备:
1.准备第四个场景相关数据
2.准备创建 DOM 的热点标记函数
(这里采用 DOM 的热点标记,使用 CSS3D 技术)
four: {
publicPath: 'technology/4/',
imgUrlArr: ['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
markList: [
{
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.05, 0.05],
position: [-0.35, -0.22, 0.4],
rotation: [-0.85, -0.45, -1.8],
targetAttr: 'three' // 目标场景信息对象属性
},
{
name: 'dom',
position: [0.49, 0, 0],
rotation: [0, -0.5 * Math.PI, 0],
targetAttr: 'five', // 目标场景信息对象属性
active(e) {
setMaterialCube(sceneInfoObj.five)
}
}
]
},
function createDomMark(infoObj) {
const { position, rotation, active } = infoObj
const tag = document.createElement('span')
tag.className = 'mark-style'
tag.innerHTML = '前进'
tag.style.pointerEvents = 'all'
tag.addEventListener('click', e => {
// 为了保证这个函数通用,回调数据对象中的函数代码
active(e)
})
// DOM -> 3D 物体
const tag3d = new CSS3DObject(tag)
tag3d.scale.set(1 / 800, 1 / 800, 1 / 800)
tag3d.position.set(...position)
tag3d.rotation.set(...rotation)
group.add(tag3d)
}
// 修改 setMaterialCube 内代码
markList.forEach(markObj => {
// 地板标记
if (markObj.name === 'landMark') createLandMark(markObj)
// 原生 DOM 标记
else if (markObj.name === 'dom') createDomMark(markObj)
})
六、展厅-第五个页面
1.准备第五个场景相关数据
2.准备创建 Video 的物体函数
(这里采用视频转 3D 物体技术)
(浏览器要求当前页面自动播放的视频是静音的,我们可以后续加上声音标签dom或者声音模型
导入进行控制点击声音播放)
3.控制轨道控制器拉动,旋转
five: {
publicPath: 'technology/5/',
imgUrlArr: ['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'],
markList: [
{
name: 'landMark',
imgUrl: 'other/landmark.png',
wh: [0.03, 0.03],
position: [-0.05, -0.05, 0.4],
rotation: [1.21, -0.15, -0.69],
targetAttr: 'four' // 目标场景信息对象属性
},
{
name: 'video',
imgUrl: 'video/movie.mp4',
wh: [0.2, 0.1],
position: [0.49, 0.04, 0.045],
rotation: [0, -0.5 * Math.PI, 0]
}
]
}
function createVideoMark(infoObj) {
const { imgUrl, wh, position, rotation } = infoObj
// 原生 video 承载视频
const video = document.createElement('video')
video.src = imgUrl
video.muted = true
video.addEventListener('loadedmetadata', () => {
video.play()
})
const plane = new THREE.PlaneGeometry(...wh)
const material = new THREE.MeshBasicMaterial({
map: (new THREE.VideoTexture(video))
})
const mesh = new THREE.Mesh(plane, material)
mesh.position.set(...position)
mesh.rotation.set(...rotation)
group.add(mesh)
}
markList.forEach(markObj => {
// 地板标记
if (markObj.name === 'landMark') createLandMark(markObj)
// 原生 DOM 标记
else if (markObj.name === 'dom') createDomMark(markObj)
// Video 标记
else if (markObj.name === 'video') createVideoMark(markObj)
})
https://github.com/geyixia/vr-memorial-hall
注意:video文件超过100M, git push 不上去,我没加git扩展
所以clone下来项目后需要你自己加上一个video
学程序上黑马,黑马程序员成就IT黑马,感谢黑马讲师的视频课程