1. Three.js 纹理映射:基础纹理、法线贴图、置换贴图

我将创建一个交互式的Three.js场景,展示基础纹理、法线贴图和置换贴图在三维对象上的应用效果。这个演示将帮助理解这三种纹理技术的差异和视觉效果。

1.1. 思路分析

  1. 创建一个包含多个几何体的场景,分别展示不同纹理效果

  2. 实现交互控制,允许用户切换纹理、调整参数

  3. 添加光照系统以突出纹理效果

  4. 提供对比视图,直观展示不同纹理的差异

下面是完整的实现代码:

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>

2. 功能说明

这个Three.js纹理映射演示具有以下功能:

  1. 三种纹理类型展示

  2. 交互控制

  3. 可视化场景

  4. 技术说明

您可以直接将此代码保存为HTML文件并在浏览器中打开,即可查看交互式演示。通过切换不同的纹理类型和调整参数,可以直观地理解各种纹理映射技术的差异和应用场景。