我将创建一个交互式的Three.js场景,展示基础纹理、法线贴图和置换贴图在三维对象上的应用效果。这个演示将帮助理解这三种纹理技术的差异和视觉效果。
创建一个包含多个几何体的场景,分别展示不同纹理效果
实现交互控制,允许用户切换纹理、调整参数
添加光照系统以突出纹理效果
提供对比视图,直观展示不同纹理的差异
下面是完整的实现代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 纹理映射:基础纹理、法线贴图、置换贴图</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #4cc9f0, #4361ee);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.subtitle {
font-size: 1.2rem;
color: #b8c1ec;
max-width: 800px;
margin: 0 auto 20px;
}
.scene-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 30px;
}
.scene-wrapper {
flex: 1;
min-width: 300px;
height: 400px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
background: #0f1525;
position: relative;
}
#scene {
width: 100%;
height: 100%;
}
.scene-label {
position: absolute;
top: 15px;
left: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 8px 15px;
border-radius: 20px;
font-weight: bold;
font-size: 1.1rem;
z-index: 10;
}
.controls-panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
}
.controls-title {
font-size: 1.5rem;
margin-bottom: 20px;
color: #4cc9f0;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.control-group {
background: rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: 8px;
}
.control-title {
font-size: 1.2rem;
margin-bottom: 15px;
color: #b8c1ec;
display: flex;
align-items: center;
}
.control-title i {
margin-right: 10px;
color: #4cc9f0;
}
.slider-container {
margin-bottom: 15px;
}
.slider-label {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.slider-value {
font-weight: bold;
color: #4cc9f0;
}
input[type="range"] {
width: 100%;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #4cc9f0;
cursor: pointer;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
button {
padding: 12px 20px;
border: none;
border-radius: 6px;
background: rgba(76, 201, 240, 0.2);
color: #4cc9f0;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
min-width: 140px;
}
button:hover {
background: rgba(76, 201, 240, 0.4);
transform: translateY(-2px);
}
button.active {
background: #4cc9f0;
color: #0f1525;
}
.explanation {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 25px;
margin-top: 20px;
}
.explanation h3 {
color: #4cc9f0;
margin-bottom: 15px;
font-size: 1.5rem;
}
.texture-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.texture-card {
background: rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: 8px;
transition: transform 0.3s ease;
}
.texture-card:hover {
transform: translateY(-5px);
}
.texture-card h4 {
color: #b8c1ec;
margin-bottom: 10px;
font-size: 1.3rem;
display: flex;
align-items: center;
}
.texture-card h4 span {
margin-right: 10px;
font-size: 1.5rem;
}
.texture-card p {
color: #a0a7c2;
font-size: 0.95rem;
}
.displacement {
color: #f72585;
}
.normal {
color: #4cc9f0;
}
.base {
color: #ffd166;
}
footer {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: #a0a7c2;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.scene-wrapper {
min-width: 100%;
height: 350px;
}
h1 {
font-size: 2.2rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Three.js 纹理映射技术</h1>
<p class="subtitle">探索基础纹理、法线贴图和置换贴图在三维渲染中的不同效果与应用场景。交互式演示让您直观理解各种纹理技术的差异。</p>
</header>
<div class="scene-container">
<div class="scene-wrapper">
<div class="scene-label">纹理对比视图</div>
<div id="scene"></div>
</div>
</div>
<div class="controls-panel">
<h2 class="controls-title">纹理控制面板</h2>
<div class="controls-grid">
<div class="control-group">
<div class="control-title">
<span>🔧</span> 纹理类型控制
</div>
<div class="button-group">
<button id="baseTextureBtn" class="active">基础纹理</button>
<button id="normalTextureBtn">法线贴图</button>
<button id="displacementTextureBtn">置换贴图</button>
<button id="allTexturesBtn">全部纹理</button>
</div>
<p style="margin-top: 15px; color: #a0a7c2; font-size: 0.9rem;">点击按钮切换当前激活的纹理类型。</p>
</div>
<div class="control-group">
<div class="control-title">
<span>🎚️</span> 纹理参数调节
</div>
<div class="slider-container">
<div class="slider-label">
<span>置换强度:</span>
<span class="slider-value" id="displacementValue">0.5</span>
</div>
<input type="range" id="displacementSlider" min="0" max="1" step="0.01" value="0.5">
</div>
<div class="slider-container">
<div class="slider-label">
<span>法线强度:</span>
<span class="slider-value" id="normalValue">1.0</span>
</div>
<input type="range" id="normalSlider" min="0" max="2" step="0.1" value="1.0">
</div>
<div class="slider-container">
<div class="slider-label">
<span>粗糙度:</span>
<span class="slider-value" id="roughnessValue">0.5</span>
</div>
<input type="range" id="roughnessSlider" min="0" max="1" step="0.01" value="0.5">
</div>
</div>
<div class="control-group">
<div class="control-title">
<span>🌓</span> 光照与视图控制
</div>
<div class="button-group">
<button id="toggleLight">切换光照</button>
<button id="rotateToggle">自动旋转</button>
<button id="resetView">重置视图</button>
</div>
<div class="slider-container" style="margin-top: 15px;">
<div class="slider-label">
<span>光照强度:</span>
<span class="slider-value" id="lightValue">1.0</span>
</div>
<input type="range" id="lightSlider" min="0" max="2" step="0.1" value="1.0">
</div>
</div>
</div>
</div>
<div class="explanation">
<h3>纹理映射技术详解</h3>
<p>纹理映射是将2D图像映射到3D模型表面的技术,用于增加细节而不增加几何复杂度。</p>
<div class="texture-types">
<div class="texture-card">
<h4 class="base"><span>●</span> 基础纹理</h4>
<p>基础纹理(漫反射贴图)为模型表面提供颜色和图案。这是最基础的纹理类型,仅影响模型的外观颜色,不改变几何形状或光照响应。</p>
</div>
<div class="texture-card">
<h4 class="normal"><span>●</span> 法线贴图</h4>
<p>法线贴图通过改变表面法线方向来模拟细节,影响光照计算,使表面看起来有凹凸感,但实际上不改变模型的几何形状。</p>
</div>
<div class="texture-card">
<h4 class="displacement"><span>●</span> 置换贴图</h4>
<p>置换贴图通过移动顶点位置真正改变几何形状,产生真实的凹凸效果和轮廓变化,但需要较高的细分级别才能看到效果。</p>
</div>
</div>
</div>
<footer>
<p>Three.js 纹理映射演示 | 使用 Three.js r128 | 通过交互探索不同纹理技术的差异</p>
</footer>
</div>
<script>
// 全局变量
let scene, camera, renderer, controls;
let planeMesh, sphereMesh, torusMesh;
let baseTexture, normalTexture, displacementTexture;
let displacementMap = 0.5;
let normalMap = 1.0;
let roughness = 0.5;
let lightIntensity = 1.0;
let autoRotate = false;
let activeTextureType = 'all'; // 'base', 'normal', 'displacement', 'all'
let lightEnabled = true;
// 纹理加载器
const textureLoader = new THREE.TextureLoader();
// 初始化场景
function init() {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f1525);
// 创建相机
camera = new THREE.PerspectiveCamera(45, document.getElementById('scene').clientWidth / document.getElementById('scene').clientHeight, 0.1, 1000);
camera.position.set(0, 5, 12);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(document.getElementById('scene').clientWidth, document.getElementById('scene').clientHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('scene').appendChild(renderer.domElement);
// 添加轨道控制器
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 加载纹理
loadTextures();
// 创建几何体
createGeometries();
// 添加光照
createLights();
// 添加事件监听
setupEventListeners();
// 窗口大小调整处理
window.addEventListener('resize', onWindowResize);
}
// 加载纹理
function loadTextures() {
// 创建程序化纹理作为基础纹理
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
// 创建砖墙纹理
ctx.fillStyle = '#8B4513';
ctx.fillRect(0, 0, 512, 512);
// 添加砖块细节
ctx.strokeStyle = '#5D2906';
ctx.lineWidth = 10;
const brickWidth = 64;
const brickHeight = 32;
const mortar = 4;
for (let y = 0; y < canvas.height; y += brickHeight + mortar) {
for (let x = 0; x < canvas.width; x += brickWidth + mortar) {
// 交错砖块
const offset = (y / (brickHeight + mortar)) % 2 === 0 ? 0 : brickWidth / 2;
ctx.strokeRect(x + offset, y, brickWidth, brickHeight);
}
}
// 添加一些颜色变化
ctx.fillStyle = 'rgba(160, 82, 45, 0.1)';
for (let i = 0; i < 200; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 15 + 5;
ctx.fillRect(x, y, size, size);
}
baseTexture = new THREE.CanvasTexture(canvas);
baseTexture.wrapS = THREE.RepeatWrapping;
baseTexture.wrapT = THREE.RepeatWrapping;
baseTexture.repeat.set(2, 2);
// 创建程序化法线贴图
const normalCanvas = document.createElement('canvas');
normalCanvas.width = 512;
normalCanvas.height = 512;
const normalCtx = normalCanvas.getContext('2d');
// 法线贴图通常使用蓝紫色调
normalCtx.fillStyle = '#8080FF'; // 基础法线颜色(正Z方向)
normalCtx.fillRect(0, 0, 512, 512);
// 添加法线变化
for (let i = 0; i < 2000; i++) {
const x = Math.random() * normalCanvas.width;
const y = Math.random() * normalCanvas.height;
const radius = Math.random() * 10 + 5;
// 创建法线变化
const angle = Math.random() * Math.PI * 2;
const nx = Math.cos(angle) * 127 + 128;
const ny = Math.sin(angle) * 127 + 128;
normalCtx.fillStyle = `rgb(${nx}, ${ny}, 255)`;
normalCtx.beginPath();
normalCtx.arc(x, y, radius, 0, Math.PI * 2);
normalCtx.fill();
}
normalTexture = new THREE.CanvasTexture(normalCanvas);
normalTexture.wrapS = THREE.RepeatWrapping;
normalTexture.wrapT = THREE.RepeatWrapping;
normalTexture.repeat.set(2, 2);
// 创建程序化置换贴图
const displacementCanvas = document.createElement('canvas');
displacementCanvas.width = 512;
displacementCanvas.height = 512;
const displacementCtx = displacementCanvas.getContext('2d');
// 创建高度图(置换贴图)
// 使用渐变和噪点
const gradient = displacementCtx.createLinearGradient(0, 0, displacementCanvas.width, displacementCanvas.height);
gradient.addColorStop(0, '#000000');
gradient.addColorStop(0.5, '#888888');
gradient.addColorStop(1, '#000000');
displacementCtx.fillStyle = gradient;
displacementCtx.fillRect(0, 0, displacementCanvas.width, displacementCanvas.height);
// 添加噪点
displacementCtx.fillStyle = 'rgba(255, 255, 255, 0.1)';
for (let i = 0; i < 5000; i++) {
const x = Math.random() * displacementCanvas.width;
const y = Math.random() * displacementCanvas.height;
const size = Math.random() * 5 + 1;
const brightness = Math.random() * 100 + 50;
displacementCtx.fillStyle = `rgba(${brightness}, ${brightness}, ${brightness}, 0.3)`;
displacementCtx.fillRect(x, y, size, size);
}
displacementTexture = new THREE.CanvasTexture(displacementCanvas);
displacementTexture.wrapS = THREE.RepeatWrapping;
displacementTexture.wrapT = THREE.RepeatWrapping;
displacementTexture.repeat.set(2, 2);
}
// 创建几何体
function createGeometries() {
// 创建平面
const planeGeometry = new THREE.PlaneGeometry(10, 10, 100, 100);
const planeMaterial = new THREE.MeshStandardMaterial({
map: baseTexture,
normalMap: normalTexture,
displacementMap: displacementTexture,
displacementScale: displacementMap,
roughness: roughness,
metalness: 0.1
});
planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotation.x = -Math.PI / 2;
planeMesh.position.y = -2;
planeMesh.receiveShadow = true;
scene.add(planeMesh);
// 创建球体
const sphereGeometry = new THREE.SphereGeometry(1.5, 64, 64);
const sphereMaterial = new THREE.MeshStandardMaterial({
map: baseTexture,
normalMap: normalTexture,
displacementMap: displacementTexture,
displacementScale: displacementMap,
roughness: roughness,
metalness: 0.1
});
sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphereMesh.position.set(-3, 1.5, 0);
sphereMesh.castShadow = true;
sphereMesh.receiveShadow = true;
scene.add(sphereMesh);
// 创建环面
const torusGeometry = new THREE.TorusGeometry(1.5, 0.5, 32, 100);
const torusMaterial = new THREE.MeshStandardMaterial({
map: baseTexture,
normalMap: normalTexture,
displacementMap: displacementTexture,
displacementScale: displacementMap,
roughness: roughness,
metalness: 0.1
});
torusMesh = new THREE.Mesh(torusGeometry, torusMaterial);
torusMesh.position.set(3, 1.5, 0);
torusMesh.castShadow = true;
torusMesh.receiveShadow = true;
scene.add(torusMesh);
}
// 创建光照
function createLights() {
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
// 定向光
const directionalLight = new THREE.DirectionalLight(0xffffff, lightIntensity);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// 点光源
const pointLight = new THREE.PointLight(0x4cc9f0, 0.5);
pointLight.position.set(-5, 3, 5);
pointLight.castShadow = true;
scene.add(pointLight);
// 将灯光添加到场景以便后续控制
scene.userData.directionalLight = directionalLight;
scene.userData.pointLight = pointLight;
scene.userData.ambientLight = ambientLight;
}
// 更新材质
function updateMaterials() {
// 根据激活的纹理类型更新材质
let map = null;
let normalMapTex = null;
let displacementMapTex = null;
switch(activeTextureType) {
case 'base':
map = baseTexture;
break;
case 'normal':
normalMapTex = normalTexture;
break;
case 'displacement':
displacementMapTex = displacementTexture;
break;
case 'all':
map = baseTexture;
normalMapTex = normalTexture;
displacementMapTex = displacementTexture;
break;
}
// 更新所有网格的材质
[planeMesh, sphereMesh, torusMesh].forEach(mesh => {
mesh.material.map = map;
mesh.material.normalMap = normalMapTex;
mesh.material.displacementMap = displacementMapTex;
mesh.material.displacementScale = displacementMap;
mesh.material.normalScale = new THREE.Vector2(normalMap, normalMap);
mesh.material.roughness = roughness;
// 需要标记材质为需要更新
mesh.material.needsUpdate = true;
});
}
// 设置事件监听
function setupEventListeners() {
// 纹理类型按钮
document.getElementById('baseTextureBtn').addEventListener('click', () => {
setActiveTexture('base');
});
document.getElementById('normalTextureBtn').addEventListener('click', () => {
setActiveTexture('normal');
});
document.getElementById('displacementTextureBtn').addEventListener('click', () => {
setActiveTexture('displacement');
});
document.getElementById('allTexturesBtn').addEventListener('click', () => {
setActiveTexture('all');
});
// 滑块控制
document.getElementById('displacementSlider').addEventListener('input', (e) => {
displacementMap = parseFloat(e.target.value);
document.getElementById('displacementValue').textContent = displacementMap.toFixed(2);
updateMaterials();
});
document.getElementById('normalSlider').addEventListener('input', (e) => {
normalMap = parseFloat(e.target.value);
document.getElementById('normalValue').textContent = normalMap.toFixed(1);
updateMaterials();
});
document.getElementById('roughnessSlider').addEventListener('input', (e) => {
roughness = parseFloat(e.target.value);
document.getElementById('roughnessValue').textContent = roughness.toFixed(2);
updateMaterials();
});
document.getElementById('lightSlider').addEventListener('input', (e) => {
lightIntensity = parseFloat(e.target.value);
document.getElementById('lightValue').textContent = lightIntensity.toFixed(1);
if (scene.userData.directionalLight) {
scene.userData.directionalLight.intensity = lightIntensity;
}
});
// 其他控制按钮
document.getElementById('toggleLight').addEventListener('click', () => {
lightEnabled = !lightEnabled;
const btn = document.getElementById('toggleLight');
btn.textContent = lightEnabled ? '关闭光照' : '开启光照';
if (scene.userData.directionalLight) {
scene.userData.directionalLight.visible = lightEnabled;
}
if (scene.userData.pointLight) {
scene.userData.pointLight.visible = lightEnabled;
}
});
document.getElementById('rotateToggle').addEventListener('click', () => {
autoRotate = !autoRotate;
const btn = document.getElementById('rotateToggle');
btn.textContent = autoRotate ? '停止旋转' : '自动旋转';
btn.classList.toggle('active', autoRotate);
});
document.getElementById('resetView').addEventListener('click', () => {
controls.reset();
camera.position.set(0, 5, 12);
controls.update();
});
}
// 设置激活纹理
function setActiveTexture(type) {
activeTextureType = type;
// 更新按钮状态
document.getElementById('baseTextureBtn').classList.remove('active');
document.getElementById('normalTextureBtn').classList.remove('active');
document.getElementById('displacementTextureBtn').classList.remove('active');
document.getElementById('allTexturesBtn').classList.remove('active');
document.getElementById(`${type}TextureBtn`).classList.add('active');
// 更新材质
updateMaterials();
}
// 窗口大小调整处理
function onWindowResize() {
camera.aspect = document.getElementById('scene').clientWidth / document.getElementById('scene').clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(document.getElementById('scene').clientWidth, document.getElementById('scene').clientHeight);
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
if (autoRotate) {
sphereMesh.rotation.y += 0.005;
torusMesh.rotation.y += 0.005;
planeMesh.rotation.z += 0.001;
}
controls.update();
renderer.render(scene, camera);
}
// 初始化并开始动画
init();
animate();
</script>
</body>
</html>
这个Three.js纹理映射演示具有以下功能:
三种纹理类型展示:
基础纹理:提供表面的颜色和图案
法线贴图:模拟表面凹凸感,影响光照计算
置换贴图:真正改变几何形状,产生真实凹凸
交互控制:
切换不同纹理类型查看效果
调整纹理参数(置换强度、法线强度、粗糙度)
控制光照强度和开关
开启/关闭自动旋转
可视化场景:
平面、球体和圆环面三种几何体
实时阴影效果
可自由旋转的相机视角
技术说明:
使用程序化生成的纹理,无需外部图像文件
应用了Three.js的PBR材质(MeshStandardMaterial)
实现了实时参数更新和材质切换
您可以直接将此代码保存为HTML文件并在浏览器中打开,即可查看交互式演示。通过切换不同的纹理类型和调整参数,可以直观地理解各种纹理映射技术的差异和应用场景。