Threejs实现3D飞行器交互场景

最近在b站上面看到up主:gamemcu的3D作品,着实让人感到非常震撼,作品中的SU7模型,利用的是Blender进行建模,利用了webGL的技术进行开发。本次编写效果如下:

9145a9aa973549c9a0a0036cc4288f76.gif

初识Three

gamemcu大佬的作品

su7xiaomi su7 made by gamemcuicon-default.png?t=N7T8https://gamemcu.com/su7/

官网学习

利用了一天的时间,到官方网站(Three.js – JavaScript 3D Library)看了documentation、examples和editor。到b站中看了视频,学习了基础的知识。

首页也有推荐一些教学视频,比如resource中提到的Three.js Journery。也是up主:gamemcu推荐去看的,在b站中可以找到免费的教学视频

Three.js Journey(新版-双语字幕)_哔哩哔哩_bilibili
873e82716915448eaab13401f84e090d.png

源码学习

Three.js是开源的,可以把源码下载来看,官网里的所有examples在源码里面都有demo。

984a978b6dc44be9a4f96de67cce3798.png

docs:是官网的文档,网络卡的时候,可以用本地的看。

examples:放的是所有的demo,可以学习到官方的编码方式

editor:是模型编辑器,可视化编辑模型,提高在编写代码时的效率。

402396b5d1fb40a28a6588ccceba6f53.png

初识Blender

大概了解部分Three.js知识后,还需要了解一下建模的软件-Blender,这款软件功能强大,而且是开源的。当需要给模型添加一些动画效果,可能就要对模型进行修改。就可以用Blender对模型进行建模处理了。如果想更加深入,可以在Blender建好模型,添加好动画,然后直接Three.js导入Animation。

982778d3a829433b9921304e7a4cace2.png

开发作品

经过这几天的基础学习和准备,现在可以着手开发了。

搭建场景

8e0669a1fb374eae8f9594c8e1df6647.png

首先是搭建基本场景

function init() {
  // 初始化场景
    const initScene = () =>{
        scene = new THREE.Scene();
        scene.background = new THREE.Color( 0xa0a0a0 );
        scene.fog = new THREE.Fog( 0xa0a0a0, 4, 20 );
    }

    // 初始化相机
    const initCamera = () =>{
        camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
        camera.position.set( 10, 20, 10 );
    }

    // 初始化半球光
    const hemiLight = () =>{
        const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444, 3 );
        hemiLight.position.set( 0, 20, 0 );
        scene.add( hemiLight );
    }

    // 初始化平行光
    const initLight = () =>{
        dLight = new THREE.DirectionalLight( 0xffffff, 3 );
        dLight.position.set( 0, 20, 10 );
        dLight.castShadow = true;
        dLight.shadow.camera.top = 2;
        dLight.shadow.camera.bottom = - 2;
        dLight.shadow.camera.left = - 2;
        dLight.shadow.camera.right = 2;
        scene.add( dLight );

    }

    // 初始化地面
    const initGround = () =>{
        const ground = new THREE.Mesh( new THREE.PlaneGeometry( 40, 40 ), new
        THREE.MeshPhongMaterial( { color: 0xbbbbbb, depthWrite: false } ) );
        ground.rotation.x = - Math.PI / 2;
        ground.receiveShadow = true;
        scene.add( ground );
    }

    // 初始化地面网格
    const initGrid = () =>{
        const grid = new THREE.GridHelper( 40, 20, 0x000000, 0x000000 );
        grid.material.opacity = 0.2;
        grid.material.transparent = true;
        scene.add( grid );
    }

    // 初始化场景辅助标线
    const initAxesHelper = () =>{
        let axesHelper = new THREE.AxesHelper( 3 );
        scene.add( axesHelper );
    }
}

导入模型

这里提前导入了两个模型,同时利用回调,添加了加载模型过程的progress。


// 模型1
function loadStaticScene(){
    return new Promise(resolve => {
        let loader = new GLTFLoader();
        const url = "../models/gltf/WM161_zhankai_1k.glb"
        loader.load(url, (gltf) => {
            gltf.scene.position.set(0,27,0)
            gltf.scene.scale.set(15, 15, 15);
            resolve(gltf);
        },(event) =>{
            const {loaded,total} = event
            handleLoadProgress({url,loaded,total})
        });
    })
}

// 模型2
function loadStaticScene_v11(){
    return new Promise(resolve => {
        let loader = new GLTFLoader();
        const url = "../models/gltf/wm161_v11_zhedie_1k.glb"
        loader.load(url, (gltf) => {
            gltf.scene.position.set(0,-27,0)
            gltf.scene.scale.set(15, 15, 15);
            resolve(gltf);
        },(event) =>{
            const {loaded,total} = event
            handleLoadProgress({url,loaded,total})
        });
    })
}

// 用于加载
function handleLoadProgress({url, loaded, total}){
    const percentage = ((loaded / total) * 100).toFixed(2);
    if (/.*\.(blob|glb)$/i.test(url)) {
        updateLoadingProgress(`加载场景模型:${percentage}%`);
    }

}

function updateLoadingProgress(loading_text){
    progress && (progress.textContent = loading_text);
}

加载模型

bfe9c91df2554ce28ce8354aba496353.png

可用看到模型加载的时候,出现了加载进度。

loadScenes(); // 先加载模型

async function loadScenes(){
    gltf = await loadStaticScene(); // 加载模型1
    gltf_v11 = await loadStaticScene_v11(); // 加载模型2

    showLoading(); // loading过程
    handleIntoScene(); // 进入场景
}

模型被加载到场景 

58413bd71f7544119aafab3977b377b9.png

工作台

为了更换操作模型,在页面添加一个工作台。同时加上标题,丰富画面。

cdca701269664a659b12647f3814d284.gif

这是一个普通HTML编写的工作台 ,加了mac-docker动画效果

 <div class="dock_con display-none">
        <div class="dock">
            <div class="dock-item">
                <i class="icon icon_fly" data-name="起飞"></i>
            </div>
            <div class="dock-item">
                <i class="icon icon_color" data-name="颜色贴图"></i>
            </div>
            <div class="dock-item">
                <i class="icon icon_check" data-name="模型切换"></i>
            </div>
            <div class="dock-item">
                <i class="icon icon_setup" data-name="设置"></i>
            </div>
        </div>
    </div>

切换模型

点击切换的时候,利用TWEEN进行动画式的切换效果 

88d9faa0df2d4c0e82d54dddbfd3efaa.gif

function handleDockClick() {
    // 点击切换
    const handleCheck = () =>{
        isCheck = !isCheck;
        const gltf_vector = isCheck ? new THREE.Vector3(0,0,0): new THREE.Vector3(0,27,0)
        const gltf_v11_vector = isCheck ? new THREE.Vector3(0,0,0): new THREE.Vector3(0,-27,0)
        new TWEEN.Tween( gltf.scene.position)
            .to({ x: gltf_vector.x, y: gltf_vector.y, z: gltf_vector.z }, 500)
            .easing(TWEEN.Easing.Quadratic.Out)
            .start();
        new TWEEN.Tween( gltf_v11.scene.position)
            .to({ x: gltf_v11_vector.x, y: gltf_v11_vector.y, z: gltf_v11_vector.z }, 500)
            .easing(TWEEN.Easing.Quadratic.Out)
            .start();
    }

    const handleColor = () =>{}

    const handleFly = () =>{}

   // 点击事件的事件委托
    delegate(dock, 'click', 'i', function(e){
        const DOCK_MAP = [
            {
                target: ()=> e.target.classList.contains('icon_check'),
                handler: handleCheck
            },
            {
                target: ()=> e.target.classList.contains('icon_color'),
                handler: handleColor
            },
            {
                target: ()=> e.target.classList.contains('icon_fly'),
                handler: handleFly
            }
        ]
        const event = DOCK_MAP.find(item => item.target());
        if (event) {
            event.handler();
        }
    })

}

操作模型

这里给模型添加一个起飞的动画效果。(由于帧率问题gif图的机翼旋转看上去会有点奇怪)

161cac06ceac4101968839c6edf2dc74.gif

async function loadScenes(){
        gltf = await loadStaticScene();
        gltf_v11 = await loadStaticScene_v11();
        setRotateAnimation(gltf) // 缓存飞起动画
        showLoading();
        handleIntoScene();
    }

// 在加载模型的时候,先给模型加入动画进行缓存
function setRotateAnimation(gltf){
        gltf.scene.traverse((e)=>{
            const polySurface_number = e.name.replace(/polySurface/g,'')
            const rotatePoly = ['58','81','90','102']
            if (rotatePoly.includes(polySurface_number)){
                const rotation = e.rotation
                const tween  = new TWEEN.Tween( e.rotation)
                    .to({x:rotation.x,y:Math.PI * 2,z:rotation.z}, 50)
                    .easing(TWEEN.Easing.Linear.None)
                    .repeat(Infinity)
                flyPoly.push(tween)
            }
        })
    }

function handleDockClick() {
    const handleCheck = () =>{}

    const handleColor = () =>{}

    // 起飞
    const handleFly = () =>{
        isFly =  !isFly
        flyPoly.forEach(item=>{
            if (isFly){
                item && item.start()
            }else {
                item && item.stop()
            }
        })
    }

   // 点击事件的事件委托
    delegate(dock, 'click', 'i', function(e){
        const DOCK_MAP = [
            {
                target: ()=> e.target.classList.contains('icon_check'),
                handler: handleCheck
            },
            {
                target: ()=> e.target.classList.contains('icon_color'),
                handler: handleColor
            },
            {
                target: ()=> e.target.classList.contains('icon_fly'),
                handler: handleFly
            }
        ]
        const event = DOCK_MAP.find(item => item.target());
        if (event) {
            event.handler();
        }
    })

}

起飞这里为了给机翼有个由慢到快的加速效果,然后停止的时候也有个由快到慢的效果,动画的方式需要做个阻尼运动。因此,改变一下动画的方式,取消TWEEN动画,用requestAnimationFrame代替。(由于帧率问题gif图的机翼旋转看上去会有点奇怪)

dfa4c1983c084a53b278a8deb41c4a29.gif

 function flyGltf(){
        gltf.scene.traverse((e)=>{
            const polySurface_number = e.name.replace(/polySurface/g,'')
            const rotatePoly = ['60','81','90','94']
            if (rotatePoly.includes(polySurface_number)){
                e.rotation.y += flySeep
            }
        })
        // flySeep 来控制节奏
        if (flySeep <= 1 && isFly){
            flySeep += 0.002
        }else {
            flySeep -= 0.002
        }
        if (flySeep <= 0){
            flySeep = 0
        }
    }


 function animate(){
        requestAnimationFrame( animate );
        updateJump();
        controls.update();
        TWEEN.update();
        beginFly && flyGltf(); // 在requestAnimationFrame下执行动画,实现快慢节奏变换
        isLoading && updateCamera();
        !isLoading && hoverPoint();
        renderer.render( scene, camera );
    }

颜色板块

这里给页面添加一个色板,用于改变模型的颜色,当然也可以利用该方式去对模型进行其他操作

b45de9b31f114fdfafa59af84adc346c.gif

 如何知道替换颜色的模型,是哪个部位呢?可以用在上文提到过的editor编辑器,模型导入进去后,点击某个部位,就可以看到他的名称是什么了。

1876943d2d6648b78cb259163dae8728.png

function handleDockClick() {
    const handleCheck = () =>{}

     // 替换颜色。
     const handleColor = () =>{
            isAppendColor = !isAppendColor;
            if (isAppendColor){
                color_map.classList.add('show-color-map');
                delegate(color_map, 'click', 'div', function(e){
                    setColorMap(e.target.style.backgroundColor)
                })
            }else {
                color_map.classList.remove('show-color-map');
            }

            const setColorMap = (color) =>{
                gltf.scene.traverse((e)=>{
                    const polySurface_number = e.name.replace(/polySurface/g,'')
                    const windBlade = ['21','101','201'] // 给名为这几个的模型,替换颜色
                    if (windBlade.includes(polySurface_number)){
                        e.material.color.set(color)
                    }
                })
            }

        }  

    const handleFly = () =>{}

   // 点击事件的事件委托
    delegate(dock, 'click', 'i', function(e){
        const DOCK_MAP = [
            {
                target: ()=> e.target.classList.contains('icon_check'),
                handler: handleCheck
            },
            {
                target: ()=> e.target.classList.contains('icon_color'),
                handler: handleColor
            },
            {
                target: ()=> e.target.classList.contains('icon_fly'),
                handler: handleFly
            }
        ]
        const event = DOCK_MAP.find(item => item.target());
        if (event) {
            event.handler();
        }
    })

}

射线处理

射线处理,就是当鼠标移动到射线投射的位置上时候,可以显示出对应的模型信息和知道当前鼠标位置是否在某个模型上面,一般用于碰撞检测。我这里只用来显示一些大概的内容。

62ebf09a7d1441d497d9190d4c0a9073.gif

function hoverPoint(){
   if (mouse.x === 0 && mouse.y ===0) return;
   raycaster.setFromCamera(mouse, camera); // 投射到鼠标上面
   let intersects = raycaster.intersectObjects(scene.children); // 作用到场景

   if(INTERSECTED) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );

   if (intersects.length && intersects[0].object.name){
       INTERSECTED = intersects[0].object; // 用一个变量来缓存被投射到的模型信息
       // 这里改变的是变量中保存的模型信息,就不会改变到原本模型的信息,也有利用做其他操作
       INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
       INTERSECTED.material.emissive.setHex( 0x8F8BFF );
       showTip(INTERSECTED?.material.name)
   }else {
     if (INTERSECTED) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
            INTERSECTED = null;
            tip.style.display = 'none';
        }
    }
 
// 监听鼠标移动事件,这里需要对鼠标做一个处理
canvas.addEventListener('mousemove', function(event) {
  client = {
     x: event.clientX,
     y: event.clientY
  }
  clearMouse();
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1; // -1 ~ 1
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // -1 ~ 1
 });           

射线的选中效果也可以换另外一种方式,例如下面这种高亮边方式

ede740fe480e49ee9070fa249e0a8cb6.gif

const initRender = () =>{
            renderer = new THREE.WebGLRenderer( { antialias: true,canvas:canvas } );
            renderer.setPixelRatio( window.devicePixelRatio );
            renderer.setSize( window.innerWidth, window.innerHeight );
            renderer.shadowMap.enabled = true;

            // 后期处理
            composer = new EffectComposer( renderer );
            const renderPass = new RenderPass( scene, camera );
            composer.addPass( renderPass );
            outlinePass = new OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), scene, camera );
            composer.addPass( outlinePass );

            // const textureLoader = new THREE.TextureLoader();
            // textureLoader.load( '../image/tri_pattern.jpg', function ( texture ) {
            //    用贴图方式设置高亮边
            //     outlinePass.patternTexture = texture;
            //     texture.wrapS = THREE.RepeatWrapping;
            //     texture.wrapT = THREE.RepeatWrapping;
            //
            // } );

            // 高亮边的样式
            outlinePass.edgeStrength = 10.0 // 边框的亮度
            outlinePass.edgeGlow = 1 // 光晕[0,1]
            outlinePass.usePatternTexture = false // 是否使用父级的材质
            outlinePass.edgeThickness = 1.0 // 边框宽度
            outlinePass.downSampleRatio = 1 // 边框弯曲度
            outlinePass.pulsePeriod = 5 // 呼吸闪烁的速度
            outlinePass.visibleEdgeColor.set(parseInt(0x00ff00)) // 呼吸显示的颜色
            outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0) // 呼吸消失的颜色
            outlinePass.clear = true

            const outputPass = new OutputPass();
            composer.addPass( outputPass );

            effectFXAA = new ShaderPass( FXAAShader );
            effectFXAA.uniforms[ 'resolution' ].value.set( 1 / window.innerWidth, 1 / window.innerHeight );
            effectFXAA.renderToScreen = true
            composer.addPass( effectFXAA );

        }

    // 收集射线位置,调用前,先清空之前的收集
    function addSelectedObject( object ) {
        selectedObjects = [];
        selectedObjects.push( object );
    }

 function hoverPoint(){
        if (mouse.x === 0 && mouse.y ===0) return;
        raycaster.setFromCamera(mouse, camera);
        let intersects = raycaster.intersectObjects(scene.children);
        if(INTERSECTED) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
        if (intersects.length && intersects[0].object.name && intersects[0].object.name !== 'sprite'){
            INTERSECTED = intersects[0].object;
            addSelectedObject( INTERSECTED ); // 收集射线到的区域并执行高亮边
            outlinePass.selectedObjects = selectedObjects;
            // INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
            // INTERSECTED.material.emissive.setHex( 0x8F8BFF );
            showTip(INTERSECTED?.material.name)
        }else {
            // if (INTERSECTED) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
            INTERSECTED = null;
            outlinePass.selectedObjects = []
            tip.style.display = 'none';
        }
    }

动画处理

剩下的就是一些常用的CSS动画、CSS过渡动画和JS动画的细节处理了。当然这里还有一个加载完毕进场的阻尼效果。b4ccda3f7ed34eddb423910fb85a0974.gif

    function updateCamera() {
        const vector = new THREE.Vector3()
        targetPosition.addScaledVector(vector, 0.25);
        const {x,y,z} = targetPosition

        camera.position.x += (targetPosition.x - camera.position.x) * dampingFactor;
        camera.position.y += (targetPosition.y - camera.position.y) * dampingFactor;
        camera.position.z += (targetPosition.z - camera.position.z) * dampingFactor;
        const cameraX = camera.position.x.toFixed(1);
        const cameraY = camera.position.y.toFixed(1);
        const cameraZ = camera.position.z.toFixed(1);
        if (Number(cameraX) <= x && Number(cameraY) <= y && Number(cameraZ) <= z){
            isLoading = false
            title.classList.remove("display-none");
            dock_con.classList.remove("display-none");
        }

    }
  
    function animate(){
        requestAnimationFrame( animate );
        updateJump();
        controls.update();
        TWEEN.update();
        isLoading && updateCamera();
        !isLoading && hoverPoint();
        renderer.render( scene, camera );
    }

操作杆处理

给摄像机添加一个前后左右的类似操作杆的效果,然后移动的时候,视口中间出现一些跳动的文字对应操作的按键。

ed2909be8da24afdac8e38cb52cbb270.gif

 let mouse = new THREE.Vector2();
    let velocity = new THREE.Vector3();
    let up_vector = new THREE.Vector3(0, 1, 0);
    let temp_vector = new THREE.Vector3();
    let targetPosition = new THREE.Vector3(1.8, 1.9, 3.4);

// 跳动的文字
const throttledScrollHandler = throttle(function() {
    const id = document.querySelector('.text-up')
    new TextUp( {id:id,text: keyCode.toUpperCase()} ) // 在节流的控制下生成文字
}, 500);


// 操作杆
document.addEventListener('keydown', function(event) {
    keyCode = event.key === ' '? 'space' : event.key
    camera.position.addScaledVector(velocity, 0.5);
    const angle = controls.getAzimuthalAngle();
    const speed = 0.25
    mouse = new THREE.Vector2(0,0)
    switch (event.key) {
                case 'w':
                    temp_vector.set(0, 0, -1).applyAxisAngle(up_vector, angle);
                    camera.position.addScaledVector(temp_vector, speed);
                    throttledScrollHandler()
                    break;
                case 's':
                    temp_vector.set(0, 0, 1).applyAxisAngle(up_vector, angle);
                    camera.position.addScaledVector(temp_vector, speed);
                    throttledScrollHandler()
                    break;
                case 'a':
                    temp_vector.set(-1, 0, 0).applyAxisAngle(up_vector, angle);
                    camera.position.addScaledVector(temp_vector, speed);
                    throttledScrollHandler()
                    break;
                case 'd':
                    temp_vector.set(1, 0, 0).applyAxisAngle(up_vector, angle);
                    camera.position.addScaledVector(temp_vector, speed);
                    throttledScrollHandler()
                    break;
                case ' ':
                    jump();
                    throttledScrollHandler()
                    break;
            }
        });

    

  function updateJump(){
        if (isJumping) {
            camera.position.y += jumpVelocity;
            jumpVelocity -= 0.05;
            if (camera.position.y <= 1.5) {
                camera.position.y = 1.5;
                isJumping = false;
            }
        }
    }

    function jump() {
        if (!isJumping) {
            jumpVelocity = 0.3 * maxJumpHeight
            console.log(jumpVelocity);
            isJumping = true;
        }
    }

 动态反射

给地面添加一个模型反射效果和一个shader反射效果。

418224de9ec44670b93d302406106b3d.gif

 材质添加反光贴图

cubeCamera.position.copy(camera.position);

cubeCamera的位置设置为与常规相机camera相同的位置。在Three.js中,每个对象(包括相机)都有一个.position属性,它是一个Vector3对象,表示该对象在三维空间中的位置。.copy()方法用于复制另一个Vector3对象的位置到当前对象。

cubeCamera.position.y *= -1;

cubeCamera的y坐标(即高度)取反。换句话说,如果cubeCamera原本在y=10的位置,执行这行代码后,它会被移动到y=-10的位置。这通常用于确保立方体相机能够捕获场景的背面(即常规相机背后的场景),因为立方体相机需要拍摄场景的六个方向(前、后、左、右、上、下)。

cubeCamera.update(renderer, scene);

更新cubeCamera的六个纹理面。它告诉cubeCamera在当前的位置和方向上,使用给定的renderer(渲染器)和scene(场景)来捕获场景的六个方向的图像,并将这些图像存储在cubeCamera的立方体纹理中。

// 新建一个立方相机cubeCamera,用于存储立方体相机捕获的图像
cubeRenderTarget = new THREE.WebGLCubeRenderTarget(1024);
cubeRenderTarget.texture.type = THREE.HalfFloatType;
cubeCamera = new THREE.CubeCamera(0.1,200,cubeRenderTarget);

const initGround = () =>{
    const planeGeometry = new THREE.PlaneGeometry( 40, 40 )
    const meshPhongMaterial = new THREE.MeshStandardMaterial( {
        color: 0x333333,
        depthWrite: false,
        roughness: 0.1,
        metalness: 0.5,
        envMap: cubeRenderTarget.texture,   // 给材质添加一个环境贴图,实现反光效果
    } )
    const ground = new THREE.Mesh( planeGeometry, meshPhongMaterial);
    ground.rotation.x = - Math.PI / 2;
    ground.receiveShadow = true;
    addShader( meshPhongMaterial ); // 给地面加入shader
    scene.add( ground );
}

function animate(){
    requestAnimationFrame( animate );
   // 确保cubeCamera与常规相机camera在同一位置,但高度相反,然后更新cubeCamera的立方体纹理以 
   // 捕获场景的六个方向的图像。
    cubeCamera.position.copy(camera.position);
    cubeCamera.position.y *= -1;
    cubeCamera.update(renderer,scene);
}

加入shader 

vertexShader1: 开始部分的顶点着色器代码。

vertexShader3: 顶点着色器中的一段替换代码,包含 #include <fog_vertex> 的替换。

fragmentShader1: 开始部分的片段着色器代码。

fragmentShader2: 片段着色器中的一段替换代码,包含 #include <dithering_fragment> 的替换。

function addShader(material){
    const vertexShader1 = `uniform float uSize;
          varying vec2 vUv;
          void main() {`;
    const vertexShader3 = `#include <fog_vertex>
                    vUv=vec2(-position.x,-position.y)/uSize;`;
    const fragmentShader1 = `varying vec2 vUv;
            uniform float uTime;
            uniform vec3 uColor;
            uniform float uSize;
            void main() {`;
    const fragmentShader2 = `#include <dithering_fragment>
        float d=length(vUv);
              if(d >= uTime&&d<=uTime+ 0.1) {
                gl_FragColor.rgb = gl_FragColor.rgb+mix(uColor,gl_FragColor.rgb,1.0-(d-uTime)*10.0 )*0.5  ;
              }`;
    material.onBeforeCompile = (shader) => {
        shaders.push(shader);
        shader.uniforms.uSize = { value: 50 };
        shader.uniforms.uTime = { value: 0.1 };
        shader.uniforms.uColor = { value: new THREE.Color('#4ff9ff') };
        shader.vertexShader = shader.vertexShader.replace('void main() {', vertexShader1);
        shader.vertexShader = shader.vertexShader.replace(
            '#include <fog_vertex>',
            vertexShader3
        );
        shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragmentShader1);
        shader.fragmentShader = shader.fragmentShader.replace(
            '#include <dithering_fragment>',
            fragmentShader2
        );
    };
}

function animate(){

    if (shaders?.length) {
        // 着色器动画
        shaders.forEach((shader) => {
            shader.uniforms.uTime.value += 0.008;
            if (shader.uniforms.uTime.value >= 1) {
                shader.uniforms.uTime.value = 0;
            }
        });
    }
}

截取图片保存 

通过获取场景,来实现拍照功能

fc0cfa1e0e1f4e1d8d982092c59766ea.png

    function getGltfUrl() {
      return new Promise((resolve) => {
        renderer.render(scene, camera)
        resolve(canvas.toDataURL('image/png'))
      })
    },
    const handleTakePicture = async ()=> {
      imageMask.src = await getGltfUrl();
      photoMask.classList.remove('display-none');
      showWhiteScreen()
    }

源码地址

下载源码的时候,记得顺手留下个star ♥♥

GitHub - Trevor-Han/basis-three: 基于threejs的模型交互,包括方向控制、颜色贴图、阻尼效果、TWEEN动画、JavaScript动画、CSS3过渡等效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值