WebGL 密码盒:解锁 soul 星球效果的创意密钥

3D soul星球效果 )这个 HTML 文件展示了如何使用 Three.js 创建一个 3D 场景,其中包括一个半透明的球体和多个可交互的小球体,每个小球体上都有一个随机生成的昵称。用户可以通过鼠标或触摸与这些小球体进行交互,查看它们的标签。这是一个展示 Three.js 基本功能和交互性的示例。

html 复制代码
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8" />
    <title>soul星球</title>
    <style>
        body {
            margin: 0;
            background-color: black;
            touch-action: none;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
    <script type="module">
        import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js'

        // 创建场景
        const scene = new THREE.Scene()

        // 创建相机
        const camera = new THREE.PerspectiveCamera(
            75,
            window.innerWidth / window.innerHeight,
            0.1,
            1000
        )
        camera.position.set(0, 0, 14)
        camera.lookAt(0, 0, 0)

        // 创建渲染器
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
        renderer.setSize(window.innerWidth, window.innerHeight)
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.setClearColor(0x000000, 0)
        document.body.appendChild(renderer.domElement)

        // 创建半透明球体
        const sphereGeometry = new THREE.SphereGeometry(4.85, 16, 16)
        const sphereMaterial = new THREE.ShaderMaterial({
            uniforms: {
                color: { value: new THREE.Color(0x000000) },
                opacity: { value: 0.8 },
            },
            vertexShader: `
          varying vec3 vNormal;
          void main() {
              vNormal = normalize(normalMatrix * normal);
              gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
          }
        `,
            fragmentShader: `
          uniform vec3 color;
          uniform float opacity;
          varying vec3 vNormal;
          void main() {
              float alpha = opacity * smoothstep(0.5, 1.0, vNormal.z);
              gl_FragColor = vec4(color, alpha);
          }
        `,
            transparent: true,
            side: THREE.FrontSide,
            depthWrite: false,
        })

        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
        scene.add(sphere)

        // 创建小球体和标签数组
        const smallBallGeometry = new THREE.SphereGeometry(0.15, 16, 16)
        const smallBalls = []
        const labelSprites = []

        const radius = 5
        const numPoints = 88
        const goldenRatio = (1 + Math.sqrt(5)) / 2
        const maxWidth = 160
        const textSpeed = 0.002

        // 创建射线投射器
        const raycaster = new THREE.Raycaster()
        const mouse = new THREE.Vector2()

        function createTextTexture(text, parameters = {}) {
            const {
                fontSize = 24,
                fontFace = 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
                textColor = 'white',
                backgroundColor = 'rgba(0,0,0,0)',
                maxWidth = 160,
            } = parameters

            const canvas = document.createElement('canvas')
            const context = canvas.getContext('2d')
            context.font = `${fontSize}px ${fontFace}`

            const textMetrics = context.measureText(text)
            const textWidth = Math.ceil(textMetrics.width)
            const textHeight = fontSize * 1.2

            const needMarquee = textWidth > maxWidth

            let canvasWidth = maxWidth
            if (needMarquee) {
                canvasWidth = textWidth + 60
            }

            canvas.width = canvasWidth
            canvas.height = textHeight
            context.font = `${fontSize}px ${fontFace}`
            context.clearRect(0, 0, canvas.width, canvas.height)

            context.fillStyle = backgroundColor
            context.fillRect(0, 0, canvas.width, canvas.height)

            context.fillStyle = textColor
            context.textAlign = needMarquee ? 'left' : 'center'
            context.textBaseline = 'middle'

            if (needMarquee) {
                context.fillText(text, 0, canvas.height / 2)
            } else {
                context.fillText(text, maxWidth / 2, canvas.height / 2)
            }

            const texture = new THREE.CanvasTexture(canvas)
            texture.needsUpdate = true

            if (needMarquee) {
                texture.wrapS = THREE.RepeatWrapping
                texture.wrapT = THREE.ClampToEdgeWrapping
                texture.repeat.x = maxWidth / canvas.width
            } else {
                texture.wrapS = THREE.ClampToEdgeWrapping
                texture.wrapT = THREE.ClampToEdgeWrapping
            }

            texture.minFilter = THREE.LinearFilter
            texture.magFilter = THREE.LinearFilter
            texture.generateMipmaps = false
            return { texture, needMarquee, HWRate: textHeight / maxWidth }
        }

        for (let i = 0; i < numPoints; i++) {
            const y = 1 - (i / (numPoints - 1)) * 2
            const radiusAtY = Math.sqrt(1 - y * y)

            const theta = (2 * Math.PI * i) / goldenRatio

            const x = Math.cos(theta) * radiusAtY
            const z = Math.sin(theta) * radiusAtY
            const smallBallMaterial = new THREE.MeshBasicMaterial({
                color: getRandomBrightColor(),
                depthWrite: true,
                depthTest: true,
                side: THREE.FrontSide,
            })
            const smallBall = new THREE.Mesh(smallBallGeometry, smallBallMaterial)
            smallBall.position.set(x * radius, y * radius, z * radius)
            sphere.add(smallBall)
            smallBalls.push(smallBall)

            const labelText = getRandomNickname()
            const { texture, needMarquee, HWRate } = createTextTexture(labelText, {
                fontSize: 38,
                fontFace: 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
                textColor: '#FFFFFF',
                maxWidth: maxWidth,
            })

            const spriteMaterial = new THREE.SpriteMaterial({
                map: texture,
                transparent: true,
                depthWrite: true,
                depthTest: true,
                blending: THREE.NormalBlending,
            })

            const sprite = new THREE.Sprite(spriteMaterial)
            sprite.scale.set(1, HWRate, 1)
            labelSprites.push({ sprite, smallBall, texture, needMarquee, labelText })
            scene.add(sprite)
        }

        // 添加灯光
        const light = new THREE.AmbientLight(0xffffff, 0.5)
        scene.add(light)
        const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
        directionalLight.position.set(5, 5, 5)
        scene.add(directionalLight)

        // 定义自动旋转速度和轴
        const autoRotationSpeed = 0.0005
        let autoRotationAxis = new THREE.Vector3(0, 1, 0).normalize()
        let currentAngularVelocity = autoRotationAxis.clone().multiplyScalar(autoRotationSpeed)

        let isDragging = false
        let previousMousePosition = { x: 0, y: 0 }
        let lastDragDelta = { x: 0, y: 0 }

        const decayRate = 0.92
        const increaseRate = 1.02

        // 鼠标事件处理
        const onMouseDown = (event) => {
            isDragging = true
            previousMousePosition = {
                x: event.clientX,
                y: event.clientY,
            }
        }

        const onMouseMove = (event) => {
            if (isDragging) {
                const deltaX = event.clientX - previousMousePosition.x
                const deltaY = event.clientY - previousMousePosition.y

                lastDragDelta = { x: deltaX, y: deltaY }

                const rotationFactor = 0.005

                const angleY = deltaX * rotationFactor
                const angleX = deltaY * rotationFactor

                const quaternionY = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(0, 1, 0),
                    angleY
                )
                const quaternionX = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(1, 0, 0),
                    angleX
                )

                const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)

                sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)

                const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor

                if (dragRotationAxis.length() > 0) {
                    currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
                }

                previousMousePosition = {
                    x: event.clientX,
                    y: event.clientY,
                }
            }
        }

        const onMouseUp = () => {
            if (isDragging) {
                isDragging = false

                const deltaX = lastDragDelta.x
                const deltaY = lastDragDelta.y

                if (deltaX !== 0 || deltaY !== 0) {
                    const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                    if (newAxis.length() > 0) {
                        autoRotationAxis.copy(newAxis)
                    }

                    const dragSpeed = currentAngularVelocity.length()
                    if (dragSpeed > autoRotationSpeed) {
                        // 维持当前旋转速度
                    } else {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                }
            }
        }

        // 触摸事件处理
        const onTouchStart = (event) => {
            isDragging = true
            const touch = event.touches[0]
            previousMousePosition = {
                x: touch.clientX,
                y: touch.clientY,
            }
        }

        const onTouchMove = (event) => {
            event.preventDefault()
            if (isDragging) {
                const touch = event.touches[0]
                const deltaX = touch.clientX - previousMousePosition.x
                const deltaY = touch.clientY - previousMousePosition.y

                lastDragDelta = { x: deltaX, y: deltaY }

                const rotationFactor = 0.002

                const angleY = deltaX * rotationFactor
                const angleX = deltaY * rotationFactor

                const quaternionY = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(0, 1, 0),
                    angleY
                )
                const quaternionX = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(1, 0, 0),
                    angleX
                )

                const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)

                sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)

                const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor

                if (dragRotationAxis.length() > 0) {
                    currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
                }

                previousMousePosition = {
                    x: touch.clientX,
                    y: touch.clientY,
                }
            }
        }

        const onTouchEnd = (event) => {
            if (isDragging) {
                isDragging = false

                const deltaX = lastDragDelta.x
                const deltaY = lastDragDelta.y

                if (deltaX !== 0 || deltaY !== 0) {
                    const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                    if (newAxis.length() > 0) {
                        autoRotationAxis.copy(newAxis)
                    }

                    const dragSpeed = currentAngularVelocity.length()
                    if (dragSpeed > autoRotationSpeed) {
                        // 维持当前旋转速度
                    } else {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                }
            }

            // 检查点击事件
            if (event.changedTouches.length > 0) {
                const touch = event.changedTouches[0]
                mouse.x = (touch.clientX / window.innerWidth) * 2 - 1
                mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1
                checkIntersection()
            }
        }

        // 事件监听
        window.addEventListener('mousedown', onMouseDown)
        window.addEventListener('mousemove', onMouseMove)
        window.addEventListener('mouseup', onMouseUp)
        window.addEventListener('touchstart', onTouchStart)
        window.addEventListener('touchmove', onTouchMove)
        window.addEventListener('touchend', onTouchEnd)
        document.addEventListener('gesturestart', function (e) {
            e.preventDefault()
        })

        // 添加点击事件监听
        window.addEventListener('click', onMouseClick)

        // 处理窗口大小调整
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight
            camera.updateProjectionMatrix()
            renderer.setSize(window.innerWidth, window.innerHeight)
        })

        function onMouseClick(event) {
            event.preventDefault()
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
            console.log(event.clientX, mouse.x, mouse.y)

            checkIntersection()
        }

        function checkIntersection() {
            raycaster.setFromCamera(mouse, camera)
            const intersects = raycaster.intersectObjects(smallBalls)

            if (intersects.length > 0) {
                const intersectedBall = intersects[0].object
                const index = smallBalls.indexOf(intersectedBall)
                if (index !== -1) {
                    const labelInfo = labelSprites[index]
                    showLabelInfo(labelInfo)
                }
            }
        }

        function showLabelInfo(labelInfo) {
            alert(`点击的小球标签:${labelInfo.labelText}`)
        }

        // 动画循环
        function animate() {
            requestAnimationFrame(animate)

            if (!isDragging) {
                const deltaQuat = new THREE.Quaternion().setFromEuler(
                    new THREE.Euler(
                        currentAngularVelocity.x,
                        currentAngularVelocity.y,
                        currentAngularVelocity.z,
                        'XYZ'
                    )
                )
                sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)

                const currentSpeed = currentAngularVelocity.length()

                if (currentSpeed > autoRotationSpeed) {
                    currentAngularVelocity.multiplyScalar(decayRate)

                    if (currentAngularVelocity.length() < autoRotationSpeed) {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                } else if (currentSpeed < autoRotationSpeed) {
                    currentAngularVelocity.multiplyScalar(increaseRate)

                    if (currentAngularVelocity.length() > autoRotationSpeed) {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                } else {
                    currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                }
            }

            // 更新标签的位置和跑马灯效果
            labelSprites.forEach(({ sprite, smallBall, texture, needMarquee }) => {
                smallBall.updateMatrixWorld()
                const smallBallWorldPos = new THREE.Vector3()
                smallBall.getWorldPosition(smallBallWorldPos)

                const upOffset = new THREE.Vector3(0, 0.3, 0)

                sprite.position.copy(smallBallWorldPos).add(upOffset)

                if (needMarquee) {
                    texture.offset.x += textSpeed

                    if (texture.offset.x > 1) {
                        texture.offset.x = 0
                    }
                }
            })

            renderer.render(scene, camera)
        }

        animate()

        function getRandomBrightColor() {
            const hue = Math.floor(Math.random() * 360)
            const saturation = Math.floor(Math.random() * 40 + 10)
            const lightness = Math.floor(Math.random() * 40 + 40)

            const rgb = hslToRgb(hue, saturation, lightness)

            return (rgb.r << 16) | (rgb.g << 8) | rgb.b
        }

        function hslToRgb(h, s, l) {
            s /= 100
            l /= 100

            const c = (1 - Math.abs(2 * l - 1)) * s
            const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
            const m = l - c / 2

            let r, g, b
            if (h >= 0 && h < 60) {
                r = c
                g = x
                b = 0
            } else if (h >= 60 && h < 120) {
                r = x
                g = c
                b = 0
            } else if (h >= 120 && h < 180) {
                r = 0
                g = c
                b = x
            } else if (h >= 180 && h < 240) {
                r = 0
                g = x
                b = c
            } else if (h >= 240 && h < 300) {
                r = x
                g = 0
                b = c
            } else {
                r = c
                g = 0
                b = x
            }

            return {
                r: Math.round((r + m) * 255),
                g: Math.round((g + m) * 255),
                b: Math.round((b + m) * 255),
            }
        }

        function getRandomNickname() {
            const adjectives = [
                '孤狼', '清风', '梦蝶', '墨染', '紫电',
                '流云', '星河', '竹影', '晨曦', '飞絮'
            ]
            const nouns = [
                '漫步', '飞翔', '追梦', '墨舞', '紫梦',
                '流光', '星辰', '竹林', '晨光', '飘雪'
            ]

            const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]
            const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]

            const nickname = `${randomAdjective} ${randomNoun}`

            if (nickname.length < 2) {
                return getRandomNickname()
            } else if (nickname.length > 22) {
                return nickname.slice(0, 22)
            }

            return nickname
        }
    </script>
</body>

</html>
  • JavaScript 部分 type="module: 使用 ES6 模块导入 Three.js 库。
  • import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js';从 CDN 导入 Three.js 库。
  • 创建场景、相机和渲染器:创建一个场景 scene,相机 camera 和渲染器 renderer,并设置相机和渲染器的基本属性。
  • 创建半透明球体:使用 THREE.SphereGeometry 创建球体几何体,THREE.ShaderMaterial 创建材质,并设置为半透明。
  • 创建小球体和标签数组:创建小球体的几何体 smallBallGeometry,以及用于存储小球体和标签的数组 smallBalls 和 labelSprites。
  • 射线投射器和鼠标位置:创建射线投射器 raycaster 和鼠标位置向量 mouse。
  • 创建文本纹理:createTextTexture 函数用于创建文本的纹理,可以用于小球体上的标签。
  • 动画循环:animate 函数用于更新场景中的物体位置和渲染场景。
  • 事件监听:添加鼠标和触摸事件监听,实现用户与 3D 场景的交互。
  • 随机昵称生成:getRandomNickname 函数用于生成随机昵称。
相关推荐
松涛和鸣14 分钟前
45、无依赖信息查询系统(C语言+SQLite3+HTML)
c语言·开发语言·数据库·单片机·sqlite·html
hongkid18 分钟前
React Native 如何打包正式apk
javascript·react native·react.js
菩提祖师_1 小时前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
PieroPc1 小时前
用FastAPI 后端 和 HTML/CSS/JavaScript 前端写一个博客系统 例
前端·html·fastapi
未来之窗软件服务1 小时前
幽冥大陆(九十二 )Gitee 自动化打包JS对接IDE —东方仙盟练气期
javascript·gitee·自动化·仙盟创梦ide·东方仙盟
名字越长技术越强1 小时前
html\css\js(一)
javascript·css·html
hunter14501 小时前
2026.1.4 html简单制作
java·前端·笔记·html
李少兄1 小时前
深入理解 CSS opacity 属性
前端·css
ヤ鬧鬧o.1 小时前
IDE风格的布局界面
javascript·html5
hxjhnct1 小时前
React 为什么不采用(VUE)绑定数据?
javascript·vue.js·react.js