• 【毕业季】Three.js动态相册


    活动地址:毕业季·进击的技术er

    前言
    岁月匆匆,又是一年毕业季,这次做个动态相册展示图片,放些有意思的内容,一起回忆下校园生活吧。

    预期效果

    相册展示和点选切换,利用相机旋转和移动来实现一个点击切图平滑过渡的效果。

    实现流程

    基本流程

    1、搭建场景
    2、放置图片
    3、添加鼠标事件
    4、相机运动

    工程文件

    工程文件结构如下图:
    static:存放静态资源文件
    three.js-master:为官网下载的代码包,包含所有需要用到的资源包,链接:https://github.com/mrdoob/three.js/archive/master.zip
    请添加图片描述
    index.html:页面代码

    <!DOCTYPE html>
    <html>
    
    <head>
    	<meta charset="utf-8">
    	<title>My first three.js app</title>
    	<style>
    		body {
    			margin: 0;
    		}
    	</style>
    </head>
    
    <body>
    	<script type="importmap">
    			{
    				"imports": {
    					"three": "./three.js-master/build/three.module.js"
    				}
    			}
    		</script>
    	<script type="module">
    		// 下文JS代码位置
    		// ...
    	</script>
    </body>
    
    </html>
    
    
    • 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
    • 28
    • 29

    搭建场景

    需要导入的内容和提前声明的变量,包含后续不同function中可能用到的场景、相机、渲染器、控制器等要素。

    import * as THREE from "three";
    import { OrbitControls } from "./three.js-master/examples/jsm/controls/OrbitControls.js";
    import { GLTFLoader } from "./three.js-master/examples/jsm/loaders/GLTFLoader.js";
    import { TWEEN } from "./three.js-master/examples/jsm/libs/tween.module.min.js";
    
    let scene, camera, renderer, controls; //场景、相机、渲染器、控制器
    let pointLight, ambientLight; //光源
    let curve = null, rate = 0; // 照片点位
    let imgArr = []; //照片url
    let imgCut = 50; //照片剪影大小
    let rotateImg = true; // 是否继续轮转照片,其实是相机在转
    const imgGroup = new THREE.Group(); //照片对象组
    const textureLoader = new THREE.TextureLoader(); // 纹理加载器
    const pointer = new THREE.Vector2(); //点击坐标
    const raycaster = new THREE.Raycaster(); //射线
    const threeEl = document.getElementById('container'); //元素获取
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    场景和部分数据初始化,其中图片这里直接是使用的网络资源图,可以使用本地文件或者自己搭建的服务环境文件。

    function init() {
        imgArr = [
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg95.699pic.com%2Fphoto%2F40142%2F4204.gif_wh860.gif&refer=http%3A%2F%2Fimg95.699pic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659077726&t=ea5e3abb8b838546ae9377321744f875",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F019b795b2cc0dba80121bbec95a340.jpg%402o.jpg&refer=http%3A%2F%2Fimg.zcool.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076627&t=be25ec64df151d68c5dd5296a1d68fcf",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fp4.itc.cn%2Fimages01%2F20200707%2Fcc1bec607b1949549f374f3e0e68bc2d.jpeg&refer=http%3A%2F%2Fp4.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659072591&t=34a56c58c383f15a5118d81859cad417",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F019b795b2cc0dba80121bbec95a340.jpg%402o.jpg&refer=http%3A%2F%2Fimg.zcool.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=aae752e553bbdeda27a4fa14b242e4e8",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fp.qpic.cn%2Fdnfbbspic%2F0%2Fdnfbbs_dnfbbs_dnf_gamebbs_qq_com_forum_202007_05_084137qjj5sjd9pqm9mprr.jpg%2F0&refer=http%3A%2F%2Fp.qpic.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=bd12aa3f0d060cc19880158e9ef7b16f",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.keaidian.com%2Fuploads%2Fallimg%2F190713%2F13174657_15.jpg&refer=http%3A%2F%2Fwww.keaidian.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=47296d32109bbffce6844b557a109d24",
            "https://img2.baidu.com/it/u=3487630334,1818100496&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500",
            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic_source%2F29%2F09%2F31%2F290931cc8b21f13ff0ca273ff8e4865e.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=5c77914a690c1b53de57c7bf5692487a",
        ]
        changeImg(threeEl, imgArr[0]);
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        renderer = new THREE.WebGLRenderer();
        // 添加相机并设置在原点
        camera.position.set(0, 0, 0);
        camera.lookAt(new THREE.Vector3(1, 0, 0));
        // 添加一个点光源
        pointLight = new THREE.PointLight(0xffffff);
        scene.add(pointLight);
        // 添加一个环境光
        ambientLight = new THREE.AmbientLight(0xffffff);
        scene.add(ambientLight);
    
        // 增加坐标系红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
        // 添加坐标系到场景中
        // const axes = new THREE.AxesHelper(20);
        // scene.add(axes);
        // 创建渲染器对象
        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);//设置渲染区域尺寸
        threeEl.appendChild(renderer.domElement); //body元素中插入canvas对象render
        // 监听鼠标
        pointerListing();
        // 初始化控制器
        controls = new OrbitControls(camera, renderer.domElement);//创建控件对象
        controls.enabled = false;
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    放置图片

    用到了基础网格材质(MeshBasicMaterial)和贴图,用贴图加载器(TextureLoader)将图片贴到放置的平面缓冲几何体(PlaneGeometry)上,再在外层增加一个有一定透明度的立方缓冲几何体(BoxGeometry),让它看上去就像是一个相框。

    基础网格材质(MeshBasicMaterial)
    一个以简单着色(平面或线框)方式来绘制几何体的材质,这种材质不受光照的影响。

    构造函数(Constructor)
    MeshBasicMaterial( parameters : Object )
    parameters - (可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入(包括从Material继承的任何属性)。
    属性color例外,其可以作为十六进制字符串传递,默认情况下为 0xffffff(白色),内部调用Color.set(color)。

    TextureLoader
    加载texture的一个类。 内部使用ImageLoader来加载文件。

    构造函数
    TextureLoader( manager : LoadingManager )
    manager — 加载器使用的loadingManager,默认值为THREE.DefaultLoadingManager.

    // 图片贴到对应位置
    function placeImg() {
        imgGroup.name = 'imgGroup';
        // 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
        // Create a closed wavey loop
        curve = new THREE.CatmullRomCurve3([
            new THREE.Vector3(75, 0, 0),
            new THREE.Vector3(0, 0, 75),
            new THREE.Vector3(-75, 0, 0),
            new THREE.Vector3(0, 0, -75)
    
        ]);
        // centripetal、chordal和catmullrom
        curve.curveType = "catmullrom";
        curve.closed = true;//设置是否闭环
        curve.tension = 1; //设置线的张力,0为无弧度折线
    
        // // 为曲线添加材质在场景中显示出来,不添加到场景显示也不会影响运动轨迹,相当于一个Helper
        // const points = curve.getPoints(50);
        // const geometry = new THREE.BufferGeometry().setFromPoints(points);
        // const material = new THREE.LineBasicMaterial({ color: 0x000000 });
    
        // // Create the final object to add to the scene
        // const curveObject = new THREE.Line(geometry, material);
        // scene.add(curveObject);
    
        const imgNum = imgArr.length;
        rate = imgNum <= 0 ? 0 : (1 / imgNum);
    
        imgArr.forEach((item, index) => {
            const imgPosition = curve.getPointAt(rate * index);
            // 材质对象Material
            const material = new THREE.MeshBasicMaterial({
                side: THREE.DoubleSide,
                opacity: 0.8,
                transparent: true,
                name: "material" + index,
                map: textureLoader.load(item)
            });
            const mesh = new THREE.Mesh(new THREE.PlaneGeometry(imgCut, imgCut), material);
            mesh.position.set(imgPosition.x, imgPosition.y, imgPosition.z);
            mesh.rotation.y = -Math.PI / 2;
            mesh.lookAt(scene.position) //设置朝向
            mesh.name = item;
            // imgGroup.add(mesh);
            scene.add(mesh);
            // 加一个框
            const boxMaterial = new THREE.MeshBasicMaterial({
                opacity: 0.1,
                transparent: true,
                color: 0x0081cc
            });
            const boxMesh = new THREE.Mesh(new THREE.BoxGeometry(imgCut * 1.1, imgCut * 1.1, imgCut * 0.1), boxMaterial);
            boxMesh.position.set(imgPosition.x, imgPosition.y, imgPosition.z);
            boxMesh.rotation.y = -Math.PI / 2;
            boxMesh.lookAt(scene.position) //设置朝向
            boxMesh.name = item;
            imgGroup.add(boxMesh);
        })
        scene.add(imgGroup);
    
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    添加鼠标事件

    当鼠标放到相框上,贴有照片的平面几何体外层立方几何体的透明度改变,达到选中效果,选中贴图剪影时页面背景改变。

     // 背景替换
    function changeImg(element, url) {
        element.style.backgroundImage = `url(${url})`;
        // element.style.backgroundSize = '100%'; 
    }
    // 添加鼠标监听
    function pointerListing() {
        threeEl.addEventListener("pointerdown", onPointerDown);
        threeEl.addEventListener("pointermove", onPointerMove);
    }
    function onPointerMove(event) {
        // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
        pointer.set(
            (event.clientX / window.innerWidth) * 2 - 1,
            -(event.clientY / window.innerHeight) * 2 + 1
        );
        // console.log(scene);
        raycaster.setFromCamera(pointer, camera);
        const intersects = raycaster.intersectObjects(
            [...imgGroup.children],
            false
        );
        // 有照片就换背景
        if (intersects.length > 0) {
            const intersect = intersects[0];
            intersect.object.material.opacity = 0.5;
        }else{
            imgGroup.children.forEach(item => {
                item.material.opacity = 0.1;
            });
        }
    }
    function onPointerDown(event) {
        // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
        pointer.set(
            (event.clientX / window.innerWidth) * 2 - 1,
            -(event.clientY / window.innerHeight) * 2 + 1
        );
        // console.log(scene);
        raycaster.setFromCamera(pointer, camera);
        const intersects = raycaster.intersectObjects(
            [...imgGroup.children],
            false
        );
        // 有照片就换背景
        if (intersects.length > 0) {
            const intersect = intersects[0];
            // console.log(intersect.object);
            // 背景替换
            changeImg(threeEl, intersect.object.name);
            // 相机移动
            rotateImg = false;
            // 照片旋转停下来
            const changePosition = new THREE.Vector3(
                intersect.object.position.x*1.5,
                intersect.object.position.y*1.5,
                intersect.object.position.z*1.5);
            animateCamera(camera.position, changePosition);
        } else {
            // 相机回原点
            const o = new THREE.Vector3(0,0,0);
            animateCamera(camera.position, o);
            rotateImg = 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    相机运动

    相机本身在旋转,相对的图片看起来就像是在滚动播放,当点击某个图片时,相机移动到对应对象更外层,点击空白再回到原点,达到平滑切换的效果。

    function onPointerDown(event) {
        // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
        pointer.set(
            (event.clientX / window.innerWidth) * 2 - 1,
            -(event.clientY / window.innerHeight) * 2 + 1
        );
        // console.log(scene);
        raycaster.setFromCamera(pointer, camera);
        const intersects = raycaster.intersectObjects(
            [...imgGroup.children],
            false
        );
        // 有照片就换背景
        if (intersects.length > 0) {
            const intersect = intersects[0];
            // console.log(intersect.object);
            // 背景替换
            changeImg(threeEl, intersect.object.name);
            // 相机移动
            rotateImg = false;
            // 照片旋转停下来
            const changePosition = new THREE.Vector3(
                intersect.object.position.x*1.5,
                intersect.object.position.y*1.5,
                intersect.object.position.z*1.5);
            animateCamera(camera.position, changePosition);
        } else {
            // 相机回原点
            const o = new THREE.Vector3(0,0,0);
            animateCamera(camera.position, o);
            rotateImg = true;
        }
    };
    // current1 相机当前的位置
    // target1 相机的目标位置
    // current2 当前的controls的target
    // target2 新的controls的target
    function animateCamera(current1, target1,callBack) {
        var tween = new TWEEN.Tween({
            x1: current1.x, // 相机当前位置x
            y1: current1.y, // 相机当前位置y
            z1: current1.z, // 相机当前位置z
            // x2: current2.x, // 控制当前的中心点x
            // y2: current2.y, // 控制当前的中心点y
            // z2: current2.z // 控制当前的中心点z
        });
        tween.to({
            x1: target1.x, // 新的相机位置x
            y1: target1.y, // 新的相机位置y
            z1: target1.z, // 新的相机位置z
            // x2: target2.x, // 新的控制中心点位置x
            // y2: target2.y, // 新的控制中心点位置x
            // z2: target2.z // 新的控制中心点位置x
        }, 1000);
        tween.onUpdate(function (object) {
            camera.position.x = object.x1;
            camera.position.y = object.y1;
            camera.position.z = object.z1;
            // controls.target.x = object.x2;
            // controls.target.y = object.y2;
            // controls.target.z = object.z2;
            // controls.update();
        });
        tween.onComplete(function () {
            callBack && callBack()
        });
        tween.easing(TWEEN.Easing.Cubic.InOut);
        tween.start();
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    完整代码

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="utf-8">
        <title>My first three.js app</title>
        <style>
            body {
                margin: 0px;
            }
            #container {
                /* background: #000000 url("https://seopic.699pic.com/photo/50041/6756.jpg_wh1200.jpg") no-repeat; */
                background: #000000;
                background-size: 100vw 100vh;
                overflow: hidden;
                width: 100vw;
                height: 100vh;
            }
        </style>
    </head>
    
    <body>
        <div id="container">
    
        </div>
        <script type="importmap">
            {
                "imports": {
                    "three": "./three.js-master/build/three.module.js"
                }
            }
        </script>
        <script type="module">
            import * as THREE from "three";
            import { OrbitControls } from "./three.js-master/examples/jsm/controls/OrbitControls.js";
            import { GLTFLoader } from "./three.js-master/examples/jsm/loaders/GLTFLoader.js";
            import { TWEEN } from "./three.js-master/examples/jsm/libs/tween.module.min.js";
    
            let scene, camera, renderer, controls; //场景、相机、渲染器、控制器
            let pointLight, ambientLight; //光源
            let curve = null, rate = 0; // 照片点位
            let imgArr = []; //照片url
            let imgCut = 50; //照片剪影大小
            let rotateImg = true; // 是否继续轮转照片,其实是相机在转
            const imgGroup = new THREE.Group(); //照片对象组
            const textureLoader = new THREE.TextureLoader(); // 纹理加载器
            const pointer = new THREE.Vector2(); //点击坐标
            const raycaster = new THREE.Raycaster(); //射线
            const threeEl = document.getElementById('container'); //元素获取
    
            function init() {
                imgArr = [
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg95.699pic.com%2Fphoto%2F40142%2F4204.gif_wh860.gif&refer=http%3A%2F%2Fimg95.699pic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659077726&t=ea5e3abb8b838546ae9377321744f875",
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F019b795b2cc0dba80121bbec95a340.jpg%402o.jpg&refer=http%3A%2F%2Fimg.zcool.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076627&t=be25ec64df151d68c5dd5296a1d68fcf",
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fp4.itc.cn%2Fimages01%2F20200707%2Fcc1bec607b1949549f374f3e0e68bc2d.jpeg&refer=http%3A%2F%2Fp4.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659072591&t=34a56c58c383f15a5118d81859cad417",
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F019b795b2cc0dba80121bbec95a340.jpg%402o.jpg&refer=http%3A%2F%2Fimg.zcool.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=aae752e553bbdeda27a4fa14b242e4e8",
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fp.qpic.cn%2Fdnfbbspic%2F0%2Fdnfbbs_dnfbbs_dnf_gamebbs_qq_com_forum_202007_05_084137qjj5sjd9pqm9mprr.jpg%2F0&refer=http%3A%2F%2Fp.qpic.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=bd12aa3f0d060cc19880158e9ef7b16f",
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.keaidian.com%2Fuploads%2Fallimg%2F190713%2F13174657_15.jpg&refer=http%3A%2F%2Fwww.keaidian.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=47296d32109bbffce6844b557a109d24",
                    "https://img2.baidu.com/it/u=3487630334,1818100496&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500",
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic_source%2F29%2F09%2F31%2F290931cc8b21f13ff0ca273ff8e4865e.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659076255&t=5c77914a690c1b53de57c7bf5692487a",
                ]
                changeImg(threeEl, imgArr[0]);
                scene = new THREE.Scene();
                camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
                renderer = new THREE.WebGLRenderer();
                // 添加相机并设置在原点
                camera.position.set(0, 0, 0);
                camera.lookAt(new THREE.Vector3(1, 0, 0));
                // 添加一个点光源
                pointLight = new THREE.PointLight(0xffffff);
                scene.add(pointLight);
                // 添加一个环境光
                ambientLight = new THREE.AmbientLight(0xffffff);
                scene.add(ambientLight);
    
                // 增加坐标系红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
                // 添加坐标系到场景中
                // const axes = new THREE.AxesHelper(20);
                // scene.add(axes);
                // 创建渲染器对象
                renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
                renderer.setSize(window.innerWidth, window.innerHeight);//设置渲染区域尺寸
                threeEl.appendChild(renderer.domElement); //body元素中插入canvas对象render
                // 监听鼠标
                pointerListing();
                // 初始化控制器
                controls = new OrbitControls(camera, renderer.domElement);//创建控件对象
                controls.enabled = false;
            }
    
            // 背景替换
            function changeImg(element, url) {
                element.style.backgroundImage = `url(${url})`;
                // element.style.backgroundSize = '100%'; 
            }
            // 添加鼠标监听
            function pointerListing() {
                threeEl.addEventListener("pointerdown", onPointerDown);
                threeEl.addEventListener("pointermove", onPointerMove);
            }
            function onPointerMove(event) {
                // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
                pointer.set(
                    (event.clientX / window.innerWidth) * 2 - 1,
                    -(event.clientY / window.innerHeight) * 2 + 1
                );
                // console.log(scene);
                raycaster.setFromCamera(pointer, camera);
                const intersects = raycaster.intersectObjects(
                    [...imgGroup.children],
                    false
                );
                // 有照片就换背景
                if (intersects.length > 0) {
                    const intersect = intersects[0];
                    intersect.object.material.opacity = 0.5;
                }else{
                    imgGroup.children.forEach(item => {
                        item.material.opacity = 0.1;
                    });
                }
            }
            function onPointerDown(event) {
                // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
                pointer.set(
                    (event.clientX / window.innerWidth) * 2 - 1,
                    -(event.clientY / window.innerHeight) * 2 + 1
                );
                // console.log(scene);
                raycaster.setFromCamera(pointer, camera);
                const intersects = raycaster.intersectObjects(
                    [...imgGroup.children],
                    false
                );
                // 有照片就换背景
                if (intersects.length > 0) {
                    const intersect = intersects[0];
                    // console.log(intersect.object);
                    // 背景替换
                    changeImg(threeEl, intersect.object.name);
                    // 相机移动
                    rotateImg = false;
                    // 照片旋转停下来
                    const changePosition = new THREE.Vector3(
                        intersect.object.position.x*1.5,
                        intersect.object.position.y*1.5,
                        intersect.object.position.z*1.5);
                    animateCamera(camera.position, changePosition);
                } else {
                    // 相机回原点
                    const o = new THREE.Vector3(0,0,0);
                    animateCamera(camera.position, o);
                    rotateImg = true;
                }
            };
    
            // current1 相机当前的位置
            // target1 相机的目标位置
            // current2 当前的controls的target
            // target2 新的controls的target
            function animateCamera(current1, target1,callBack) {
                var tween = new TWEEN.Tween({
                    x1: current1.x, // 相机当前位置x
                    y1: current1.y, // 相机当前位置y
                    z1: current1.z, // 相机当前位置z
                    // x2: current2.x, // 控制当前的中心点x
                    // y2: current2.y, // 控制当前的中心点y
                    // z2: current2.z // 控制当前的中心点z
                });
                tween.to({
                    x1: target1.x, // 新的相机位置x
                    y1: target1.y, // 新的相机位置y
                    z1: target1.z, // 新的相机位置z
                    // x2: target2.x, // 新的控制中心点位置x
                    // y2: target2.y, // 新的控制中心点位置x
                    // z2: target2.z // 新的控制中心点位置x
                }, 1000);
                tween.onUpdate(function (object) {
                    camera.position.x = object.x1;
                    camera.position.y = object.y1;
                    camera.position.z = object.z1;
                    // controls.target.x = object.x2;
                    // controls.target.y = object.y2;
                    // controls.target.z = object.z2;
                    // controls.update();
                });
                tween.onComplete(function () {
                    callBack && callBack()
                });
                tween.easing(TWEEN.Easing.Cubic.InOut);
                tween.start();
            }
    
            // 图片贴到对应位置
            function placeImg() {
                imgGroup.name = 'imgGroup';
                // 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
                // Create a closed wavey loop
                curve = new THREE.CatmullRomCurve3([
                    new THREE.Vector3(75, 0, 0),
                    new THREE.Vector3(0, 0, 75),
                    new THREE.Vector3(-75, 0, 0),
                    new THREE.Vector3(0, 0, -75)
    
                ]);
                // centripetal、chordal和catmullrom
                curve.curveType = "catmullrom";
                curve.closed = true;//设置是否闭环
                curve.tension = 1; //设置线的张力,0为无弧度折线
    
                // // 为曲线添加材质在场景中显示出来,不添加到场景显示也不会影响运动轨迹,相当于一个Helper
                // const points = curve.getPoints(50);
                // const geometry = new THREE.BufferGeometry().setFromPoints(points);
                // const material = new THREE.LineBasicMaterial({ color: 0x000000 });
    
                // // Create the final object to add to the scene
                // const curveObject = new THREE.Line(geometry, material);
                // scene.add(curveObject);
    
                const imgNum = imgArr.length;
                rate = imgNum <= 0 ? 0 : (1 / imgNum);
    
                imgArr.forEach((item, index) => {
                    const imgPosition = curve.getPointAt(rate * index);
                    // 材质对象Material
                    const material = new THREE.MeshBasicMaterial({
                        side: THREE.DoubleSide,
                        opacity: 0.8,
                        transparent: true,
                        name: "material" + index,
                        map: textureLoader.load(item)
                    });
                    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(imgCut, imgCut), material);
                    mesh.position.set(imgPosition.x, imgPosition.y, imgPosition.z);
                    mesh.rotation.y = -Math.PI / 2;
                    mesh.lookAt(scene.position) //设置朝向
                    mesh.name = item;
                    // imgGroup.add(mesh);
                    scene.add(mesh);
                    // 加一个框
                    const boxMaterial = new THREE.MeshBasicMaterial({
                        opacity: 0.1,
                        transparent: true,
                        color: 0x0081cc
                    });
                    const boxMesh = new THREE.Mesh(new THREE.BoxGeometry(imgCut * 1.1, imgCut * 1.1, imgCut * 0.1), boxMaterial);
                    boxMesh.position.set(imgPosition.x, imgPosition.y, imgPosition.z);
                    boxMesh.rotation.y = -Math.PI / 2;
                    boxMesh.lookAt(scene.position) //设置朝向
                    boxMesh.name = item;
                    imgGroup.add(boxMesh);
                })
                scene.add(imgGroup);
    
            }
    
    
            //执行渲染操作   指定场景、相机作为参数
            function render() {
                renderer.render(scene, camera);//执行渲染操作
                if (rotateImg) {
                    camera.rotateY(0.001);//每次绕y轴旋转0.001弧度
                }
                TWEEN.update();
                // newmesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
                requestAnimationFrame(render);//请求再次执行渲染函数render
            }
    
            function initWindow() {
                let onResize = function () {
                    camera.aspect = window.innerWidth / window.innerHeight;
                    camera.updateProjectionMatrix();
                    renderer.setSize(window.innerWidth, window.innerHeight);
                };
                window.addEventListener("resize", onResize, false);
            };
            init();
            placeImg();
            initWindow();
            render();
    
        </script>
    
    </body>
    
    </html>
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286

    实现效果

    在这里插入图片描述

  • 相关阅读:
    Oracle 与 MySQL 的区别总结
    C语言从入门到精通第20天(结构体的使用)
    【第014篇】基于vue+elementui设计的在线预览文件(可预览图片、PDF、WORD、EXCEL文档)
    IntelliJ IDEA一站式配置【全】(提高开发效率)
    LeetCode --- 1437. Check If All 1‘s Are at Least Length K Places Away 解题报告
    【neovim 技巧系列】quickfix 与文本处理
    ARMv8函数传参中的bug!
    数据结构入门 — 栈
    精通代码复用:设计原则与最佳实践
    青果教务系统适配小爱课程表
  • 原文地址:https://blog.csdn.net/Mr_Bobcp/article/details/125534904