用three编写一个随风飘扬的红旗效果
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>中国国旗3D动画</title>
<script type="importmap">
{
"imports": {
"three": "https://threejs.org/build/three.module.min.js",
"three/examples/jsm/": "https://threejs.org/examples/jsm/"
}
}
</script>
<style>
body {
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: #0a0a0a;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
color: white;
font-family: Arial, sans-serif;
}
#container {
width: 100%;
height: 100%;
position: relative;
}
.controls {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
z-index: 100;
}
.controls button {
margin: 5px;
padding: 5px 10px;
background-color: #c00;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.controls button:hover {
background-color: #e00;
}
</style>
</head>
<body>
<div id="container"></div>
<div class="controls">
<button id="pauseBtn">暂停/继续</button>
<button id="resetBtn">重置</button>
<button id="zoomInBtn">放大</button>
<button id="zoomOutBtn">缩小</button>
</div>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
const container = document.getElementById("container");
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
container.clientWidth / container.clientHeight,
0.1,
1000
);
camera.position.set(0, 0, 5);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x0a0a0a);
container.appendChild(renderer.domElement);
// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.enablePan = false;
controls.maxDistance = 10;
controls.minDistance = 2;
controls.target.set(0, 0, 0);
// 窗口大小调整事件
window.onresize = () => {
renderer.setSize(container.clientWidth, container.clientHeight);
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
};
// 国旗材质
const flagTexture = new THREE.TextureLoader().load(
"./chinaFlag.jpg"
);
flagTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
// 国旗波浪效果材质
// 国旗材质 - 使用RawShaderMaterial实现高级自定义着色器效果
const flagMaterial = new THREE.RawShaderMaterial({
vertexShader: `
// 全局变换矩阵 - 用于将顶点从模型空间转换到裁剪空间
uniform mat4 projectionMatrix; // 投影矩阵,负责透视投影
uniform mat4 modelMatrix; // 模型矩阵,负责模型的位置、旋转和缩放
uniform mat4 viewMatrix; // 视图矩阵,代表相机的位置和方向
// 波浪动画控制参数
uniform vec2 uFrequency; // 波浪频率 (x和y方向的波浪密度)
uniform float uTime; // 时间变量,控制波浪动画的进度
uniform float uStrength; // 波浪强度,控制波浪的高度
// 顶点属性 - 每个顶点都有这些属性
attribute vec3 position; // 顶点的位置坐标
attribute vec2 uv; // 顶点的纹理坐标,用于采样纹理
// 传递给片段着色器的变量
varying float vDark; // 暗部强度,用于模拟波浪的阴影
varying vec2 vUv; // 纹理坐标,传递给片段着色器
void main() {
// 将顶点位置从模型空间转换到世界空间
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// 计算波浪衰减因子 - 从旗杆(左侧)到旗尾(右侧)波浪逐渐增强
float xFactor = clamp((modelPosition.x + 1.25) / 2.0, 0.0, 2.0);
// 计算波浪效果 - 使用正弦函数创建波浪运动
float vWave = sin(modelPosition.x * uFrequency.x - uTime ) * xFactor * uStrength;
// 添加次要波浪,使效果更自然
vWave += sin(modelPosition.y * uFrequency.y - uTime) * xFactor * uStrength * 0.5;
// 添加细微的垂直波动,模拟真实旗帜飘动
modelPosition.y += sin(modelPosition.x * 2.0 + uTime * 0.5) * 0.05 * xFactor;
// 应用波浪效果到Z轴(旗帜向前/向后飘动)
modelPosition.z += vWave;
// 完成顶点的坐标变换 - 从世界空间到视图空间再到裁剪空间
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
// 传递纹理坐标和波浪强度到片段着色器
vUv = uv;
vDark = vWave; // 波浪强度用于后续计算阴影效果
}
`,
fragmentShader: `
// 设置浮点数精度
precision mediump float;
// 从顶点着色器接收的插值变量
varying float vDark; // 波浪强度(暗部信息)
varying vec2 vUv; // 纹理坐标
// 纹理采样器
uniform sampler2D uTexture; // 国旗纹理
void main() {
// 从纹理中采样颜色
vec4 textColor = texture2D(uTexture, vUv);
// 应用波浪产生的暗部效果 - vDark值越大,颜色越暗
// 0.85是基础亮度,确保即使在波谷处也不会完全黑暗
textColor.rgb *= vDark + 0.85;
// 设置最终的像素颜色
gl_FragColor = textColor;
}
`,
// 其他材质属性
side: THREE.DoubleSide, // 双面渲染,确保从背面也能看到旗帜
uniforms: {
uFrequency: { value: new THREE.Vector2(3, 3) }, // 波浪频率
uTime: { value: 0 }, // 初始时间
uTexture: { value: flagTexture }, // 国旗纹理
uStrength: { value: 0.2 }, // 波浪强度
},
});
// 创建国旗几何体
const flagGeometry = new THREE.PlaneGeometry(3, 2, 64, 64);
// 创建国旗网格
const flagMesh = new THREE.Mesh(flagGeometry, flagMaterial);
flagMesh.position.set(0, 0, 0);
scene.add(flagMesh);
// 添加旗杆
const poleGeometry = new THREE.CylinderGeometry(0.02, 0.02, 2.5, 32);
const poleMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
const poleMesh = new THREE.Mesh(poleGeometry, poleMaterial);
poleMesh.position.set(-1.52, -0.25, 0);
scene.add(poleMesh);
// 添加顶部装饰
const topGeometry = new THREE.ConeGeometry(0.05, 0.15, 32);
const topMaterial = new THREE.MeshStandardMaterial({ color: 0xffd700 });
const topMesh = new THREE.Mesh(topGeometry, topMaterial);
topMesh.position.set(-1.52, 1.25, 0);
topMesh.rotation.x = Math.PI;
scene.add(topMesh);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(-1, 1, 1);
scene.add(directionalLight);
// 动画控制
let isAnimating = true;
let animationSpeed = 1.0;
// 按钮事件
document.getElementById("pauseBtn").addEventListener("click", () => {
isAnimating = !isAnimating;
});
document.getElementById("resetBtn").addEventListener("click", () => {
flagMaterial.uniforms.uTime.value = 0;
camera.position.set(0, 0, 5);
controls.target.set(0, 0, 0);
});
document.getElementById("zoomInBtn").addEventListener("click", () => {
camera.position.z = Math.max(2, camera.position.z - 0.5);
});
document.getElementById("zoomOutBtn").addEventListener("click", () => {
camera.position.z = Math.min(10, camera.position.z + 0.5);
});
// 动画循环
function animate() {
if (isAnimating) {
flagMaterial.uniforms.uTime.value += 0.06 * animationSpeed;
}
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>