马黑黑 发表于 2025-5-5 09:51

万马奔腾

<style>
        #tz { --state: running; margin: 30px 0; left: calc(50% - 81px); transform: translateX(-50%);width: clamp(600px, 90vw, 1400px); min-height: 80vh; aspect-ratio: 16/9; background: #99ddff; box-shadow: 2px 2px 10px rgba(0,0,0,.65); z-index: 1; position: relative; }
        #player { position: absolute; left: 20px; top: 20px; z-index: 9; clip-path: circle(45%); transition: filter .7s; cursor: pointer; transform-origin: 50% 100%; animation: swear 1s infinite alternate linear var(--state); }
        #player:hover { filter: hue-rotate(120deg); }
        #btnFs { right: 30px; top: 30px; color: #eee; }
        #btnFs:hover { color: red; }
        @keyframes swear {
                30% { transform: rotate(-6deg); }
                80% { transform: rotate(6deg); }
        }
</style>

<div id="tz">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=534750252" autoplay loop></audio>
        <img id="player" src="https://638183.freep.cn/638183/small/260.webp" width="10%" title="播放/暂停" />
</div>

<script type="importmap">
{
"imports": {
    "three": "https://esm.sh/three@0.176.0?target=es2022",
    "three/addons/": "https://esm.sh/three@0.176.0/addons/"
}
}
</script>

<script type="module">

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FS } from 'https://638183.freep.cn/638183/web/ku/fscreen.js';

let camera, scene, renderer, mesh, mixer, dummy;
const offset = 5000;
const timeOffsets = new Float32Array( 1024 );

for (let x = 0; x < 1024; x ++) {
        timeOffsets = Math.random() * 3;
}

const clock = new THREE.Clock(true);

init();

function init() {
        camera = new THREE.PerspectiveCamera(60, tz.offsetWidth / tz.offsetHeight, 100, 10000);
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0x99DDFF);
        scene.fog = new THREE.Fog(0x99DDFF, 5000, 10000);
        const light = new THREE.DirectionalLight(0xffffff, 1);
        light.position.set(200, 1000, 50);
        light.castShadow = true;
        light.shadow.camera.left = - 5000;
        light.shadow.camera.right = 5000;
        light.shadow.camera.top = 5000;
        light.shadow.camera.bottom = - 5000;
        light.shadow.camera.far = 2000;
        light.shadow.bias = - 0.01;
        light.shadow.camera.updateProjectionMatrix();
        scene.add(light);
        const hemi = new THREE.HemisphereLight(0x99DDFF, 0x669933, 1 / 3);
        scene.add(hemi);
        const ground = new THREE.Mesh(
                new THREE.PlaneGeometry(1000000, 1000000),
                new THREE.MeshStandardMaterial({color: 0x669933, depthWrite: true})
        );
        ground.rotation.x = - Math.PI / 2;
        ground.receiveShadow = true;
        scene.add(ground);
        const loader = new GLTFLoader();
        loader.load('https://638183.freep.cn/638183/web/3models/Horse.glb', function (glb) {
                dummy = glb.scene.children;
                mesh = new THREE.InstancedMesh(dummy.geometry, dummy.material, 1024);
                mesh.castShadow = true;
                for (let x = 0, i = 0; x < 32; x ++) {
                        for (let y = 0; y < 32; y ++) {
                                dummy.position.set(offset - 300 * x + 200 * Math.random(), 0, offset - 300 * y);
                                dummy.updateMatrix();
                                mesh.setMatrixAt(i, dummy.matrix);
                                mesh.setColorAt(i, new THREE.Color( `hsl(${Math.random() * 360}, 50%, 66%)`));
                                i ++;
                        }
                }
                scene.add(mesh);
                mixer = new THREE.AnimationMixer(glb.scene);
                const action = mixer.clipAction(glb.animations);
                action.play();
                tz.onclick = () => action.paused = aud.paused; //马停
        });
        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(tz.offsetWidth, tz.offsetHeight);
        renderer.setAnimationLoop(animate);
        tz.appendChild(renderer.domElement);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.VSMShadowMap;
        window.addEventListener('resize', onWindowResize);
}

function onWindowResize() {
        camera.aspect = tz.offsetWidth / tz.offsetHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(tz.offsetWidth, tz.offsetHeight);
}

function animate() {
        //if (aud.paused) return; //动画挺
        render();
}

function render() {
        const time = clock.getElapsedTime();
        const r = 3000;
        camera.position.set(Math.sin(time / 10) * r, 1500 + 1000 * Math.cos(time / 5), Math.cos(time / 10) * r);
        camera.lookAt(0, 0, 0);
        if (mesh) {
                for (let x = 0; x < 1024; x ++) {
                        mixer.setTime(time + timeOffsets);
                        mesh.setMorphAt(x, dummy);
                }
                mesh.morphTexture.needsUpdate = true;
        }
        renderer.render(scene, camera);
}

FS(tz, player);

</script>

马黑黑 发表于 2025-5-5 09:58

帖子代码:

<style>
        #tz { --state: running; margin: 30px 0; left: calc(50% - 81px); transform: translateX(-50%);width: clamp(600px, 90vw, 1400px); min-height: 80vh; aspect-ratio: 16/9; background: #99ddff; box-shadow: 2px 2px 10px rgba(0,0,0,.65); z-index: 1; position: relative; }
        #player { position: absolute; left: 20px; top: 20px; z-index: 9; clip-path: circle(45%); transition: filter .7s; cursor: pointer; transform-origin: 50% 100%; animation: swear 1s infinite alternate linear var(--state); }
        #player:hover { filter: hue-rotate(120deg); }
        #btnFs { right: 30px; top: 30px; color: #eee; }
        #btnFs:hover { color: red; }
        @keyframes swear {
                30% { transform: rotate(-6deg); }
                80% { transform: rotate(6deg); }
        }
</style>

<div id="tz">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=534750252" autoplay loop></audio>
        <img id="player" src="https://638183.freep.cn/638183/small/260.webp" width="10%" title="播放/暂停" />
</div>

<script type="importmap">
{
"imports": {
    "three": "https://esm.sh/three@0.176.0?target=es2022",
    "three/addons/": "https://esm.sh/three@0.176.0/addons/"
}
}
</script>

<script type="module">

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FS } from 'https://638183.freep.cn/638183/web/ku/fscreen.js';

let camera, scene, renderer, mesh, mixer, dummy;
const offset = 5000;
const timeOffsets = new Float32Array( 1024 );

for (let x = 0; x < 1024; x ++) {
        timeOffsets = Math.random() * 3;
}

const clock = new THREE.Clock(true);

init();

function init() {
        camera = new THREE.PerspectiveCamera(60, tz.offsetWidth / tz.offsetHeight, 100, 10000);
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0x99DDFF);
        scene.fog = new THREE.Fog(0x99DDFF, 5000, 10000);
        const light = new THREE.DirectionalLight(0xffffff, 1);
        light.position.set(200, 1000, 50);
        light.castShadow = true;
        light.shadow.camera.left = - 5000;
        light.shadow.camera.right = 5000;
        light.shadow.camera.top = 5000;
        light.shadow.camera.bottom = - 5000;
        light.shadow.camera.far = 2000;
        light.shadow.bias = - 0.01;
        light.shadow.camera.updateProjectionMatrix();
        scene.add(light);
        const hemi = new THREE.HemisphereLight(0x99DDFF, 0x669933, 1 / 3);
        scene.add(hemi);
        const ground = new THREE.Mesh(
                new THREE.PlaneGeometry(1000000, 1000000),
                new THREE.MeshStandardMaterial({color: 0x669933, depthWrite: true})
        );
        ground.rotation.x = - Math.PI / 2;
        ground.receiveShadow = true;
        scene.add(ground);
        const loader = new GLTFLoader();
        loader.load('https://638183.freep.cn/638183/web/3models/Horse.glb', function (glb) {
                dummy = glb.scene.children;
                mesh = new THREE.InstancedMesh(dummy.geometry, dummy.material, 1024);
                mesh.castShadow = true;
                for (let x = 0, i = 0; x < 32; x ++) {
                        for (let y = 0; y < 32; y ++) {
                                dummy.position.set(offset - 300 * x + 200 * Math.random(), 0, offset - 300 * y);
                                dummy.updateMatrix();
                                mesh.setMatrixAt(i, dummy.matrix);
                                mesh.setColorAt(i, new THREE.Color( `hsl(${Math.random() * 360}, 50%, 66%)`));
                                i ++;
                        }
                }
                scene.add(mesh);
                mixer = new THREE.AnimationMixer(glb.scene);
                const action = mixer.clipAction(glb.animations);
                action.play();
                tz.onclick = () => action.paused = aud.paused; //马停
        });
        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(tz.offsetWidth, tz.offsetHeight);
        renderer.setAnimationLoop(animate);
        tz.appendChild(renderer.domElement);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.VSMShadowMap;
        window.addEventListener('resize', onWindowResize);
}

function onWindowResize() {
        camera.aspect = tz.offsetWidth / tz.offsetHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(tz.offsetWidth, tz.offsetHeight);
}

function animate() {
        //if (aud.paused) return; //动画停
        render();
}

function render() {
        const time = clock.getElapsedTime();
        const r = 3000;
        camera.position.set(Math.sin(time / 10) * r, 1500 + 1000 * Math.cos(time / 5), Math.cos(time / 10) * r);
        camera.lookAt(0, 0, 0);
        if (mesh) {
                for (let x = 0; x < 1024; x ++) {
                        mixer.setTime(time + timeOffsets);
                        mesh.setMorphAt(x, dummy);
                }
                mesh.morphTexture.needsUpdate = true;
        }
        renderer.render(scene, camera);
}

FS(tz, player);

</script>动态效果源自 three.js 的一个示例,马匹建模是 three.js 官方开源的数据。JS资源借用 esm.sh 上的部署。

我在代码中加入马匹奔跑、暂停的机制。若需要整个动画都可以暂停、继续,可以解开下面代码的注释:

    //if (aud.paused) return; //动画停


同时,将马匹暂停、继续的代码注释掉:


    tz.onclick = () => action.paused = aud.paused; //马停


整个动画暂停/继续机制不是很理想,表现在继续的时候,由于动画渲染的速度太快,衔接有点突兀。

红影 发表于 2025-5-5 10:48

我的天啊,一排排一列列,居然这么多狂奔的马匹啊,太壮观了{:4_199:}

红影 发表于 2025-5-5 10:50

马黑黑 发表于 2025-5-5 09:58
帖子代码:

动态效果源自 three.js 的一个示例,马匹建模是 three.js 官方开源的数据。JS资源借用 esm.s ...

还好啊,衔接还不算突兀{:4_187:}

红影 发表于 2025-5-5 10:51

能拥有这么多骏马的人绝对是大富豪啊{:4_173:}

梦江南 发表于 2025-5-5 11:46

哇塞,真的是万马奔腾,这场面太壮观了!好上春晚去了!{:4_199:}

马黑黑 发表于 2025-5-5 12:03

梦江南 发表于 2025-5-5 11:46
哇塞,真的是万马奔腾,这场面太壮观了!好上春晚去了!

你跟春晚导演熟吗{:4_170:}

马黑黑 发表于 2025-5-5 12:04

红影 发表于 2025-5-5 10:48
我的天啊,一排排一列列,居然这么多狂奔的马匹啊,太壮观了

感觉在这里打开有点资源吃紧。我博客里会轻松一些。

马黑黑 发表于 2025-5-5 12:05

红影 发表于 2025-5-5 10:51
能拥有这么多骏马的人绝对是大富豪啊

three.js 应有尽有,是正统的土豪

马黑黑 发表于 2025-5-5 12:05

红影 发表于 2025-5-5 10:50
还好啊,衔接还不算突兀

{:4_190:}

小辣椒 发表于 2025-5-5 14:54

这个场景有点震撼的,气势出来了,万马奔腾

小辣椒 发表于 2025-5-5 14:56

还排的特别整齐{:4_170:}

杨帆 发表于 2025-5-5 16:13

整个动画衔接自然呀,谢谢马老师的大佬级分享{:4_191:}

花飞飞 发表于 2025-5-5 16:52

这里的动画停止后画面依然旋转,我刚才自己试的时候是连旋转都停了。
博客里的暂停后原地起跑,这里的位置有一丢丢差别

花飞飞 发表于 2025-5-5 16:59

气势磅礴,壮观{:4_199:}
背景给了个浅蓝色渐变,与战马下方的绿地融在一起区分天地,效果逼真。。
代码是复杂的,看着烧脑~~
我很好奇画马的代码在哪里,是不是glb这个文件。这个后缀的面生,头一回见到

花飞飞 发表于 2025-5-5 17:00

最近是饱了眼福了,一个贴子比一个贴子炫酷。。。
前所未有的视觉盛宴{:4_173:}

马黑黑 发表于 2025-5-5 18:07

花飞飞 发表于 2025-5-5 17:00
最近是饱了眼福了,一个贴子比一个贴子炫酷。。。
前所未有的视觉盛宴

这个感觉有点耗资源

花飞飞 发表于 2025-5-5 19:22

马黑黑 发表于 2025-5-5 18:07
这个感觉有点耗资源

电脑有压力,主要是我看不出来,点开吧,都正常。。
话说这背景添得好,跟天马似的。。

红影 发表于 2025-5-5 19:51

马黑黑 发表于 2025-5-5 12:04
感觉在这里打开有点资源吃紧。我博客里会轻松一些。

哦,一会去你博客看看去{:4_187:}

红影 发表于 2025-5-5 19:52

马黑黑 发表于 2025-5-5 12:05
three.js 应有尽有,是正统的土豪

太壮观了,在黑黑的贴子里领略了three.js 的强大{:4_199:}
页: [1] 2 3 4 5 6
查看完整版本: 万马奔腾