马黑黑 发表于 2025-6-11 21:27

Mess

<style>
        #tz { margin: 30px 0; left: calc(50% - 81px); transform: translateX(-50%); width: clamp(600px, 90vw, 1400px); height: auto; aspect-ratio: 16/9; background: #eee url('https://638183.freep.cn/638183/t24/w4/mess.webp') no-repeat center/cover; box-shadow: 2px 2px 8px #000; display: grid; place-items: center; z-index: 1; position: relative; }
        #btnFs { bottom: 20px; color: #eee; text-align: center; }
        #btnFs:hover { color: red; }
        #player { position: absolute; left: -1000px; }
        #tz canvas { position: absolute; }
</style>

<div id="tz">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=1328537196" autoplay loop></audio>
        <div id="player" title="播放/暂停"></div>
</div>

<script type="module">
        import * as THREE from 'https://638183.freep.cn/638183/3dev/build/three.module.min.js';
        import { FS } from 'https://638183.freep.cn/638183/web/ku/FS.js';

        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(45, tz.offsetWidth/tz.offsetHeight, 0.1, 1000);
        camera.position.set(0, 0, 10);
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(tz.offsetWidth, tz.offsetHeight);
        const clock = new THREE.Clock();
        tz.appendChild(renderer.domElement);

        const playerTexture = new THREE.TextureLoader().load('https://638183.freep.cn/638183/web/svg/sunfl-1.svg');
        const spriteTexture = new THREE.TextureLoader().load('https://638183.freep.cn/638183/web/svg/sunfl-2.svg');

        const mesh = new THREE.Mesh(
                new THREE.SphereGeometry(1, 64, 64),
                new THREE.MeshBasicMaterial({ map: playerTexture, transparent: true })
        );
        mesh.position.set(-2.15, 2.5, 0);
        mesh.rotateZ(-Math.PI / 2);
        scene.add(mesh);

        const group = new THREE.Group();
        for (let i = 0; i < 200; i++) {
                const sprite = new THREE.Sprite(
                        new THREE.SpriteMaterial({ color: Math.random() * 0xffffff, map: spriteTexture })
                );
                sprite.scale.set(0.5, 0.5, 0.5);
                sprite.position.set(
                        THREE.MathUtils.randFloatSpread(15),
                        THREE.MathUtils.randFloatSpread(15),
                        THREE.MathUtils.randFloatSpread(10)
                );
                group.add(sprite);
        }
        scene.add(group);

        const animate = () => {
                requestAnimationFrame(animate);
                const delta = clock.getDelta();
                group.rotation.x += delta / 10;
                group.rotation.y += delta / 10;
                group.rotation.z += delta / 10;
                mesh.rotation.y += delta;
                group.children.forEach(sprite => {
                        sprite.material.rotation -= delta;
                });
                renderer.render(scene, camera);
        };

        const isMess = (event) => {
                const raycaster = new THREE.Raycaster();
                const pointer = new THREE.Vector2();
                let intersects = [];
                pointer.x = (event.offsetX / tz.offsetWidth) * 2 - 1;
                pointer.y = -(event.offsetY / tz.offsetHeight) * 2 + 1;
                raycaster.setFromCamera(pointer, camera);
                intersects = raycaster.intersectObjects(, true);
                return intersects.length > 0;
        }

        tz.onmousemove = (e) => {
                isMess(e)
                        ? (tz.style.cursor = 'pointer', tz.title = '播放/暂停 Alt+X')
                        : (tz.style.cursor = 'default', tz.title = '');
        };

        tz.onclick = (e) => {
                if (isMess(e)) player.click();
        };

        window.onresize = () => {
                camera.aspect = tz.offsetWidth / tz.offsetHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(tz.offsetWidth, tz.offsetHeight);
        };

        document.onvisibilitychange = () => {
                if (aud.paused) return;
                document.visibilityState === 'hidden' ? clock.stop() : clock.start();
        };

        aud.onplaying = aud.onpause = () => aud.paused ? clock.stop() : clock.start();

        animate();
        FS(tz, player);
</script>

马黑黑 发表于 2025-6-11 21:27

帖子代码

<style>
        #tz { margin: 30px 0; left: calc(50% - 81px); transform: translateX(-50%); width: clamp(600px, 90vw, 1400px); height: auto; aspect-ratio: 16/9; background: #eee url('https://638183.freep.cn/638183/t24/w4/mess.webp') no-repeat center/cover; box-shadow: 2px 2px 8px #000; display: grid; place-items: center; z-index: 1; position: relative; }
        #btnFs { bottom: 20px; color: #eee; text-align: center; }
        #btnFs:hover { color: red; }
        #player { position: absolute; left: -1000px; }
        #tz canvas { position: absolute; }
</style>

<div id="tz">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=1328537196" autoplay loop></audio>
        <div id="player" title="播放/暂停"></div>
</div>

<script type="module">
        import * as THREE from 'https://638183.freep.cn/638183/3dev/build/three.module.min.js';
        import { FS } from 'https://638183.freep.cn/638183/web/ku/FS.js';

        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(45, tz.offsetWidth/tz.offsetHeight, 0.1, 1000);
        camera.position.set(0, 0, 10);
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(tz.offsetWidth, tz.offsetHeight);
        const clock = new THREE.Clock();
        tz.appendChild(renderer.domElement);

        const playerTexture = new THREE.TextureLoader().load('https://638183.freep.cn/638183/web/svg/sunfl-1.svg');
        const spriteTexture = new THREE.TextureLoader().load('https://638183.freep.cn/638183/web/svg/sunfl-2.svg');

        const mesh = new THREE.Mesh(
                new THREE.SphereGeometry(1, 64, 64),
                new THREE.MeshBasicMaterial({ map: playerTexture, transparent: true })
        );
        mesh.position.set(-2.15, 2.5, 0);
        mesh.rotateZ(-Math.PI / 2);
        scene.add(mesh);

        const group = new THREE.Group();
        for (let i = 0; i < 200; i++) {
                const sprite = new THREE.Sprite(
                        new THREE.SpriteMaterial({ color: Math.random() * 0xffffff, map: spriteTexture })
                );
                sprite.scale.set(0.5, 0.5, 0.5);
                sprite.position.set(
                        THREE.MathUtils.randFloatSpread(15),
                        THREE.MathUtils.randFloatSpread(15),
                        THREE.MathUtils.randFloatSpread(10)
                );
                group.add(sprite);
        }
        scene.add(group);

        const animate = () => {
                requestAnimationFrame(animate);
                const delta = clock.getDelta();
                group.rotation.x += delta / 10;
                group.rotation.y += delta / 10;
                group.rotation.z += delta / 10;
                mesh.rotation.y += delta;
                group.children.forEach(sprite => {
                        sprite.material.rotation -= delta;
                });
                renderer.render(scene, camera);
        };

        const isMess = (event) => {
                const raycaster = new THREE.Raycaster();
                const pointer = new THREE.Vector2();
                let intersects = [];
                pointer.x = (event.offsetX / tz.offsetWidth) * 2 - 1;
                pointer.y = -(event.offsetY / tz.offsetHeight) * 2 + 1;
                raycaster.setFromCamera(pointer, camera);
                intersects = raycaster.intersectObjects(, true);
                return intersects.length > 0;
        }

        tz.onmousemove = (e) => {
                isMess(e)
                        ? (tz.style.cursor = 'pointer', tz.title = '播放/暂停 Alt+X')
                        : (tz.style.cursor = 'default', tz.title = '');
        };

        tz.onclick = (e) => {
                if (isMess(e)) player.click();
        };

        window.onresize = () => {
                camera.aspect = tz.offsetWidth / tz.offsetHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(tz.offsetWidth, tz.offsetHeight);
        };

        document.onvisibilitychange = () => {
                if (aud.paused) return;
                document.visibilityState === 'hidden' ? clock.stop() : clock.start();
        };

        aud.onplaying = aud.onpause = () => aud.paused ? clock.stop() : clock.start();

        animate();
        FS(tz, player);
</script>

马黑黑 发表于 2025-6-11 21:27

本帖最后由 马黑黑 于 2025-6-11 22:37 编辑

本帖:

一、播放控制器

简单的球体几何+基础材质搭建的Mesh王哥图像:


        const mesh = new THREE.Mesh(
                new THREE.SphereGeometry(1, 64, 64),
                new THREE.MeshBasicMaterial({ map: playerTexture, transparent: true })
        );
        mesh.position.set(-2.15, 2.5, 0);
        mesh.rotateZ(-Math.PI / 2);
        scene.add(mesh);



位置设置 position.set() 基于帖子背景图像的特点,效果正如大家所看到的。-2.15 表示从中心向左移动的距离单位、2.5 是从中心向上移动的距离单位,距离单位基于经验,当然也结合相机在 Z轴 上的定位(camera.position.set(0, 0, 10););Z轴上的翻转 rotateZ() 出于构图需求,也可以通过翻转 texture 贴图实现。

二、粒子系统

粒子采用精灵 Sprite 做成,它永远面朝相机,就是说我们总是看到它的正面、无论它怎么翻转——如果它可以翻转。精灵放在一个 ThreeJS 组(Group)里,使用 ThreeJS 封装的工具集合 MathUtils 的方法 randFloatSpread() 进行xyz三个方向的快速随机定位。相机在 Z轴 的 10 个距离单位上,以此为参照,XY轴 都取 15 个距离单位以便令精灵存在略微超越边界的可能,Z轴 上则取 10 个距离单位。精灵在动画中随 group 进行xyz三个方向的旋转,也可以考虑仅在一个或两个方向上旋转,不论如何,理论上都会随机出现个体较大的情形。

粒子的大致个体尺寸可以通过 spirte.scale.set(0.5, 0.5, 0.5); 进行设置,但运动中的精灵粒子还是会在特定角度下出现较大的情况。另外,由于场景xyz轴的尺寸并不一致,运动过程中产生的效果非常迷人。

红影 发表于 2025-6-11 21:41

这球体贴图有意思,用这样的svg图图竟然能贴出半个球体的感觉,这个没想到{:4_187:}

朵拉 发表于 2025-6-11 22:11

精彩之作,赞一个{:4_178:}

花飞飞 发表于 2025-6-11 22:24

这个粒子飞舞的方式不仅是上下,而是四面八方~有的上有的下,且前后空间感立体感超强~特别好看~

杨帆 发表于 2025-6-11 22:29

漂亮,谢谢马老师经典分享{:4_191:}

花飞飞 发表于 2025-6-11 22:30

这个漂亮的太阳花红色和绿色,
半球体效果像是燃烧的火焰一样,又特别又好看~
绿色的也被随机渲染成各种色彩,这个怎么做到的~
异域音乐神秘

马黑黑 发表于 2025-6-11 22:39

红影 发表于 2025-6-11 21:41
这球体贴图有意思,用这样的svg图图竟然能贴出半个球体的感觉,这个没想到

图片有透明部分。开启 transparent: true 属性之后,透明部分就会透明,不开启的话透明部分是黑色

马黑黑 发表于 2025-6-11 22:42

花飞飞 发表于 2025-6-11 22:30
这个漂亮的太阳花红色和绿色,
半球体效果像是燃烧的火焰一样,又特别又好看~
绿色的也被随机渲染成各种色 ...

当材质设置有颜色和贴图,贴图的颜色和所设置的颜色会互相叠加。随机颜色中可以进行有限的控制,比方只需要红色参与对贴图的影响:

new THREE.SpriteMaterial({ color: Math.random() * 0xff0000, map: spriteTexture })

这样,材质中贴图随机以红色系叠加到自身

马黑黑 发表于 2025-6-11 22:42

杨帆 发表于 2025-6-11 22:29
漂亮,谢谢马老师经典分享

{:4_190:}

马黑黑 发表于 2025-6-11 22:44

花飞飞 发表于 2025-6-11 22:24
这个粒子飞舞的方式不仅是上下,而是四面八方~有的上有的下,且前后空间感立体感超强~特别好看~

精灵全部加入都组 group 中。就像微信群一样,加入了,群里的活动大家共同参与。group 在 xyz 三个方向都有旋转,精灵跟群旋转。

马黑黑 发表于 2025-6-11 22:44

朵拉 发表于 2025-6-11 22:11
精彩之作,赞一个

{:4_191:}

樵歌 发表于 2025-6-12 07:05

美!简直美得要让你滴们学生把你爱S{:4_172:}

马黑黑 发表于 2025-6-12 08:20

樵歌 发表于 2025-6-12 07:05
美!简直美得要让你滴们学生把你爱S

{:4_196:}{:4_203:}

红影 发表于 2025-6-12 14:58

马黑黑 发表于 2025-6-11 22:39
图片有透明部分。开启 transparent: true 属性之后,透明部分就会透明,不开启的话透明部分是黑色

这个效果非常奇妙,不是帖上得到效果,空想很难想得到是这样{:4_173:}

樵歌 发表于 2025-6-12 16:39

马黑黑 发表于 2025-6-12 08:20


瞧把把都乐成猪头了{:4_172:}

马黑黑 发表于 2025-6-12 18:33

樵歌 发表于 2025-6-12 16:39
瞧把把都乐成猪头了

相当于捡了块大号的玉石

马黑黑 发表于 2025-6-12 18:36

红影 发表于 2025-6-12 14:58
这个效果非常奇妙,不是帖上得到效果,空想很难想得到是这样

我花了不少闲暇时间去读文档

花飞飞 发表于 2025-6-12 20:15

马黑黑 发表于 2025-6-11 22:42
当材质设置有颜色和贴图,贴图的颜色和所设置的颜色会互相叠加。随机颜色中可以进行有限的控制,比方只需 ...

原来如此,今天那个花型的却有不同层次的色彩。。内圈和外圈不同。。
看了源码是本身花型就是这样的,你找的图还真是经典
页: [1] 2
查看完整版本: Mess