three.js学习笔记(二十二)——混合HTML和WebGL

介绍

这次课将学习如何将HTML集成到场景中去——即一个交互式的HTML元素跟随场景中的3D位置而变化,看起来就像嵌在WebGL中一般。

初始设置

采用上一节教的带有进度条的加载动画,并且使用头盔模型。

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { gsap } from 'gsap'

/**
 * Loaders
 */
const loadingBarElement = document.querySelector('.loading-bar')
const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        // Wait a little
        window.setTimeout(() =>
        {
            // Animate overlay
            gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0, delay: 1 })

            // Update loadingBarElement
            loadingBarElement.classList.add('ended')
            loadingBarElement.style.transform = ''
        }, 500)
    },

    // Progress
    (itemUrl, itemsLoaded, itemsTotal) =>
    {
        // Calculate the progress and update the loadingBarElement
        const progressRatio = itemsLoaded / itemsTotal
        loadingBarElement.style.transform = `scaleX(${progressRatio})`
    }
)
const gltfLoader = new GLTFLoader(loadingManager)
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager)

/**
 * Base
 */
// Debug
const debugObject = {}

// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Overlay
 */
const overlayGeometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1)
const overlayMaterial = new THREE.ShaderMaterial({
    // wireframe: true,
    transparent: true,
    uniforms:
    {
        uAlpha: { value: 1 }
    },
    vertexShader: `
        void main()
        {
            gl_Position = vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform float uAlpha;

        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha);
        }
    `
})
const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial)
scene.add(overlay)

/**
 * Update all materials
 */
const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            // child.material.envMap = environmentMap
            child.material.envMapIntensity = debugObject.envMapIntensity
            child.material.needsUpdate = true
            child.castShadow = true
            child.receiveShadow = true
        }
    })
}

/**
 * Environment map
 */
const environmentMap = cubeTextureLoader.load([
    '/textures/environmentMaps/0/px.jpg',
    '/textures/environmentMaps/0/nx.jpg',
    '/textures/environmentMaps/0/py.jpg',
    '/textures/environmentMaps/0/ny.jpg',
    '/textures/environmentMaps/0/pz.jpg',
    '/textures/environmentMaps/0/nz.jpg'
])

environmentMap.encoding = THREE.sRGBEncoding

scene.background = environmentMap
scene.environment = environmentMap

debugObject.envMapIntensity = 5

/**
 * Models
 */
gltfLoader.load(
    '/models/DamagedHelmet/glTF/DamagedHelmet.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(2.5, 2.5, 2.5)
        gltf.scene.rotation.y = Math.PI * 0.5
        scene.add(gltf.scene)

        updateAllMaterials()
    }
)

/**
 * Lights
 */
const directionalLight = new THREE.DirectionalLight('#ffffff', 3)
directionalLight.castShadow = true
directionalLight.shadow.camera.far = 15
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.normalBias = 0.05
directionalLight.position.set(0.25, 3, - 2.25)
scene.add(directionalLight)

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.set(4, 1, - 4)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true
})
renderer.physicallyCorrectLights = true
renderer.outputEncoding = THREE.sRGBEncoding
renderer.toneMapping = THREE.ReinhardToneMapping
renderer.toneMappingExposure = 3
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Animate
 */
const tick = () =>
{
    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

HTML

首先在html文件添加一个类为point的元素,后面还会继续添加其他点元素point-1point-2……这样子。

<canvas class="webgl"></canvas>

<div class="loading-bar"></div>

<div class="point point-0">
    <div class="label">1</div>
    <div class="text">Lorem ipsum, dolor sit amet consectetur adipisicing elit</div>
</div>

CSS

.point
{
    position: absolute;
    top: 50%;
    left: 50%;
}

.point .label
{
    position: absolute;
    top: -20px;
    left: -20px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: #00000077;
    border: 1px solid #ffffff77;
    color: #ffffff;
    font-family: Helvetica, Arial, sans-serif;
    text-align: center;
    line-height: 40px;
    font-weight: 100;
    font-size: 14px;
    cursor: help;
}

.point .text
{
    position: absolute;
    top: 30px;
    left: -120px;
    width: 200px;
    padding: 20px;
    border-radius: 4px;
    background: #00000077;
    border: 1px solid #ffffff77;
    color: #ffffff;
    line-height: 1.3em;
    font-family: Helvetica, Arial, sans-serif;
    font-weight: 100;
    font-size: 14px;
    opacity: 0;
    transition: opacity 0.3s;
    pointer-events: none;
}

.point:hover .text
{
    opacity: 1;
}

添加样式,达成下图效果:
在这里插入图片描述
由于后边还要通过缩放隐藏这些点元素,因此要给它们一个样式来控制在页面中的缩放达成显隐,修改css如下:

.point .label
{
    /* ... */
    transform: scale(0, 0);
    transition: transform 0.3s;
}

.point.visible .label
{
    transform: scale(1, 1);
}

元素要想显示就加上样式:

<div class="point point-0 visible">

JavaScript

存储点元素

使用数组来存储页面上的点元素,每个数组元素对象有两个属性:

  • position:用来存放3D位置坐标
  • element: 从HTML获取对应点元素的节点
const points = [
    {
        position: new THREE.Vector3(1.55, 0.3, - 0.6),
        element: document.querySelector('.point-0')
    }
]

更新点位置

我们要在tick函数中直接更新每个帧上的点元素节点。

const tick = () =>
{
    // Update controls
    controls.update()

    // 遍历每个点
    for(const point of points)
    {

    }

    // ...
}

我们需要根据场景中点的3D坐标来获取点在屏幕上的二维坐标。

第一步,使用clone()方法克隆点的位置得到screenPosition,再对screenPosition调用project(...) 方法并传入摄像机camera作为参数:

const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        console.log(screenPosition.x)
    }

    // ...
}

打印该点的x坐标,可以看到其值非常接近于0。按住右键把头盔拖往屏幕左侧,值将接近-1,拖往屏幕右侧,值将接近+1。

接下去要把这个坐标转换成像素值,因为要根据像素值去改变页面元素的位置。
要把场景中的点坐标转换为屏幕中的像素,需要乘以渲染器尺寸的一半:

const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        const translateX = screenPosition.x * sizes.width * 0.5
        console.log(translateX)
    }

    // ...
} 

更新元素的transform属性,在x轴上平移了translateX个像素:

for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        const translateX = screenPosition.x * sizes.width * 0.5
        point.element.style.transform = `translateX(${translateX}px)`
    }

y轴同理,但是要注意的是在css中,y轴下方代表正值,而在Three.js中则是普通的坐标系y轴上方代表正值,因此translateY要取反:

const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        const translateX = screenPosition.x * sizes.width * 0.5
        const translateY = - screenPosition.y * sizes.height * 0.5
        point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`
    }

    // ...
}

在这里插入图片描述

点的显隐

当有东西挡住该点在场景中的3D坐标时,元素应该隐藏起来。

先移除元素的visible样式类。之后,为了测试点的前面是否有东西,我们将使用光线投射Raycaster
我们将从摄像机发射一条射线到对应点,如果没有东西与射线相交,则显示该点;若东西与射线相交,则计算与该交点的距离,如果交点比点元素距离更远,则说明点元素在交点前面,继续显示,如果交点离摄像机距离更近,则说明点元素被覆盖住了,要移除visible样式将点给隐藏起来。

在tick函数外部实例化光线投射,然后在函数内部更新raycaster
要让射线从摄像机发射到对应点,需要调用setFromCamera(...)方法,方法第一个参数是对应点在屏幕上的二维坐标,第二个参数则是射线来源的相机。
在这里,虽然screenPosition是一个Vector3,但是我们只用到他的x和y属性:

const raycaster = new THREE.Raycaster()
// ...
const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        raycaster.setFromCamera(screenPosition, camera)

        // ...
    }

    // ...
}

使用intersectObject(...)方法来检查与射线相交的物体,这里要传入俩个参数,第一个是scene.children,场景中所有物体,第二个参数为true代表开启递归测试,会递归遍历物体的子代。

const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        raycaster.setFromCamera(screenPosition, camera)
        const intersects = raycaster.intersectObjects(scene.children, true)

        // ...
    }

    // ...
}

先判断射线是否有相交物体,没有的话则添加visible类显示该点,若有相交物体,再做后续其他判断:

const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        raycaster.setFromCamera(screenPosition, camera)
        const intersects = raycaster.intersectObjects(scene.children, true)

        if(intersects.length === 0)
        {
            point.element.classList.add('visible')
        }
        else
        {
            
        }

        // ...
    }

    // ...
}

intersectObjects(...)方法会返回一个相交对象数组,这些对象会按照距离排序,这也就意味着我们无需去测试全部相交对象,只需要测试第一个便可,通过distance属性获取距离值。
然后通过distanceTo(...)方法传入相机位置,获取点元素到相机的距离值。
接着比较两个值大小,决定点元素的显隐:

const tick = () =>
{
    // ...

    for(const point of points)
    {
        const screenPosition = point.position.clone()
        screenPosition.project(camera)

        raycaster.setFromCamera(screenPosition, camera)
        const intersects = raycaster.intersectObjects(scene.children, true)

        if(intersects.length === 0)
        {
            point.element.classList.add('visible')
        }
        else
        {
            const intersectionDistance = intersects[0].distance
            const pointDistance = point.position.distanceTo(camera.position)
			if(intersectionDistance < pointDistance)
            {
                point.element.classList.remove('visible')
            }
            else
            {
                point.element.classList.add('visible')
            }
        }

        // ...
    }

    // ...
}

现在已经可以说是几乎完成了,但是在页面刷新加载进度条时,场景尚未加载完毕,但那些点元素是显示着的。
要解决这个问题很简单,设置一个值为false的变量,然后在tick函数中使用。只有当资源加载完毕后,再设为true,去更新点元素坐标:

let sceneReady = false
const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        // ...

        window.setTimeout(() =>
        {
            sceneReady = true
        }, 2000)
    },

    // ...
)

// ...

const tick = () =>
{
    // ...

    if(sceneReady)
    {
        for(const point of points)
        {
            // ...
        }
    }

    // ...
}

现在可以去增加其他点元素节点并在js中设置它们的坐标了:

<div class="point point-0">
    <div class="label">1</div>
    <div class="text">Front and top screen with HUD aggregating terrain and battle informations.</div>
</div>
<div class="point point-1">
    <div class="label">2</div>
    <div class="text">Ventilation with air purifier and detection of environment toxicity.</div>
</div>
<div class="point point-2">
    <div class="label">3</div>
    <div class="text">Cameras supporting night vision and heat vision with automatic adjustment.</div>
</div>
const points = [
    {
        position: new THREE.Vector3(1.55, 0.3, - 0.6),
        element: document.querySelector('.point-0')
    },
    {
        position: new THREE.Vector3(0.5, 0.8, - 1.6),
        element: document.querySelector('.point-1')
    },
    {
        position: new THREE.Vector3(1.6, - 1.3, - 0.7),
        element: document.querySelector('.point-2')
    }
]

最终效果

在这里插入图片描述

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,我会为您解答关于WebGL three.js的阴影与实现物体动画的问题。首先,让我们来了解一下WebGL three.js是什么。 WebGL three.js是一款基于WebGLJavaScript 3D库,可以帮助我们快速搭建3D场景和应用。接下来我们来讲解阴影和实现物体动画的方法。 一、阴影 阴影是模拟物体之间的阴影效果,让3D场景更加真实。在three.js中,我们可以通过设置Mesh的castShadow和receiveShadow属性来实现阴影效果。 1. 首先,我们需要在场景中添加光源,例如SpotLight或DirectionalLight。 2. 然后,在需要投射阴影的物体上设置castShadow为true。 3. 最后,在需要接收阴影的物体上设置receiveShadow为true。 代码示例: ```javascript // 添加光源 const light = new THREE.SpotLight(0xffffff); light.position.set(0, 100, 0); light.castShadow = true; scene.add(light); // 添加需要投射阴影的物体 const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshLambertMaterial({ color: 0xff0000 })); cube.castShadow = true; scene.add(cube); // 添加需要接收阴影的物体 const plane = new THREE.Mesh(new THREE.PlaneGeometry(200, 200, 1, 1), new THREE.MeshLambertMaterial({ color: 0xffffff })); plane.receiveShadow = true; plane.rotation.x = -Math.PI / 2; scene.add(plane); ``` 二、物体动画 在three.js中,我们可以通过Tween.js库来实现物体的动画效果。Tween.js是一款JavaScript动画库,可以帮助我们实现非常丰富的动画效果。 1. 首先,我们需要在HTML文件中引入Tween.js库文件。 2. 然后,在需要动画的物体上设置初始状态。 3. 最后,通过Tween.js库来设置物体的目标状态和动画效果,例如缓动动画(ease)或弹跳动画(bounce)。 代码示例: ```javascript // 引入Tween.js库文件 <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.min.js"></script> // 添加需要动画的物体 const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshLambertMaterial({ color: 0xff0000 })); cube.position.set(0, 0, 0); scene.add(cube); // 设置初始状态 const start = { x: 0, y: 0, z: 0 }; // 设置目标状态 const end = { x: 50, y: 50, z: 50 }; // 设置动画效果 const tween = new TWEEN.Tween(start) .to(end, 1000) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { cube.position.set(start.x, start.y, start.z); }) .start(); ``` 以上是关于WebGL three.js阴影与实现物体动画的方法,希望能够对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值