4 месяца назад — 30 января я увидел страницу https://resend.com с потрясающим 3D-кубом. Мой мозг тут же включился, пытаясь понять, как они это сделали.

4 месяца спустя их недавний запуск напомнил мне о моем желании украсть их (украсть как отсылку к художнику, ура!). Поскольку у меня есть некоторый опыт работы с Three.js, я сразу же начал.

Начните с шаблона

Это настраивает вас на самую простую сцену — один белый куб, черный фон и стандартный материал на кубе, что означает, что вы должны видеть тени и прочее. Довольно аккуратно. Пожалуйста. (На самом деле я украл это и немного адаптировал из 3dcodekits; спасибо 3dcodekits, я люблю вас!).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Resend Cube</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script>
    // Create the scene
    const scene = new THREE.Scene();
    scene.background = new THREE.Color( 0x000000 );    
    // Create the light
    const light = new THREE.DirectionalLight( 0xffffff );
    light.position.set( 0, 0.5, 1 );
    scene.add( light );
    // Create the camera
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    // Create the renderer
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    // Create a cube
    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);
    // Position the camera
    camera.position.z = 10;
    // Animate the cube
    function animate() {
        requestAnimationFrame(animate);
        cube.rotation.x += 0.005;
        cube.rotation.y += 0.005;
        renderer.render(scene, camera);
    }
    animate();
</script>
</body>
</html>

Давайте сделаем куб менее кубическим (более круглым).

Оказывается, три js не имеют прямого решения этой проблемы. Итак, быстрый поиск в гугле выдал эту изящную маленькую функцию (источник)

function createBoxWithRoundedEdges( width, height, depth, radius0, smoothness ) {
    let shape = new THREE.Shape();
    let eps = 0.00001;
    let radius = radius0 - eps;
    shape.absarc( eps, eps, eps, -Math.PI / 2, -Math.PI, true );
    shape.absarc( eps, height -  radius * 2, eps, Math.PI, Math.PI / 2, true );
    shape.absarc( width - radius * 2, height -  radius * 2, eps, Math.PI / 2, 0, true );
    shape.absarc( width - radius * 2, eps, eps, 0, -Math.PI / 2, true );
    let geometry = new THREE.ExtrudeBufferGeometry( shape, {
        amount: depth - radius0 * 2,
        bevelEnabled: true,
        bevelSegments: smoothness * 2,
        steps: 1,
        bevelSize: radius,
        bevelThickness: radius0,
        curveSegments: smoothness
    });
    geometry.center();
    return geometry;
}

При вызове функции получается аккуратный маленький куб с закругленными углами. вы можете настроить степень скругления углов с помощью 4-го параметра — radius0. (не забудьте отрегулировать плавность по мере увеличения радиуса. Вы поймете, почему, как только сделаете это)

const geometry = createBoxWithRoundedEdges( 1, 1, 1, .06, 20 ); //the smoother the better

Материал + свет

Теперь мы хотим, чтобы куб ощущался как тот, что был повторно отправлен — это означает, что нам нужно сделать его металлическим и с другим светом. С металлом все просто — мы уже используем стандартный материал, поэтому просто добавляем немного металличности и сглаживаем шероховатость (заодно меняем цвет на темно-серый).

const material = new THREE.MeshStandardMaterial({ color: 0x2a2a2a, metalness: 1, roughness: 0.11 });

Теперь куб сильно блекнет — он почти сливается с фоном, и вы иногда видите какие-то вспышки. Нам нужно заменить источник света точечным источником света и настроить его так, чтобы он всегда освещал какую-то часть куба.

По сравнению с направленным светом, который излучается в определенном направлении (но он достаточно слаб и падает на грань куба, создавая эффект «фонарика» — вы видите, как воздействуют на небольшую поверхность), точечный свет излучает свет во всех направлениях, что создает хороший эффект, который мы можем использовать — свет, отраженный в кубе, будет больше и всегда попадет на поверхности — края для еще более крутого эффекта. Мы создаем довольно большой точечный источник света, способный осветить всю сторону куба.

Мы также можем добавить второй свет для полутонов. Размещение его под кубом гарантирует, что какая-то часть всегда будет выделена.

// Create the light
const light = new THREE.PointLight( 0xffffff, 100, 500 );
light.position.set( 0, 10, 10 );
scene.add( light );
const light2 = new THREE.PointLight( 0xffffff, 10, 500 );
light2.position.set( 0, -10, -5 );
scene.add( light2 );

Вы можете поиграть со свойствами материала и освещением (интенсивностью, цветом), чтобы создать интересные эффекты.

Изготовление детских кубиков

А вот и самое интересное. Мы создадим еще одну функцию, которая вызовет функцию куба с закругленными краями в некоторых вложенных циклах и создаст 27 кубов. Пойдем.

function makeCubes() {
    const material = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, metalness: 1, roughness: 0.18 });
    const numCubes = 3;
    // Create the group, we will add cubes to the group
    const cubes = new THREE.Group();
    // iterate over all dimensions + trick to center the cube
    for (let i = -Math.floor(numCubes / 2); i <= Math.floor(numCubes / 2); i++) {
        for (let j = -Math.floor(numCubes / 2); j <= Math.floor(numCubes / 2); j++) {
            for (let k = -Math.floor(numCubes / 2); k <= Math.floor(numCubes / 2); k++) {
                // adding the cubes
                const geom = createBoxWithRoundedEdges(1, 1, 1, .1, 20);
                geom.translate(i, j, k);
                const cube = new THREE.Mesh(geom, material);
                cubes.add(cube);
            }
        }
    }
    return cubes;
}

Все, что нам нужно сделать сейчас, это вызвать его:

// Create a cube
const cube = makeCubes();
scene.add(cube);

Добавление элементов управления

В качестве бонуса мы можем добавить элементы управления и вращать куб, как нам нравится. Если вы хотите отключить авторотацию, просто закомментируйте две строки cube.rotation.

Сначала нам нужно импортировать скрипт аддонов. Поместите эту строку под тремя скриптами импорта.

<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three-addons.min.js"></script>

Затем добавляем элементы управления — можно добавить где угодно, например рядом с линией camera=….

const controls = new THREE_ADDONS.OrbitControls(camera,renderer.domElement);

И, наконец, мы редактируем функцию анимации, которая вызывается каждый раз, когда что-то рендерится.

// Animate the cube
function animate() {
    requestAnimationFrame(animate);
    cube.rotation.x += 0.005;
    cube.rotation.y -= 0.005;
    controls.update();
    renderer.render(scene, camera);
}

Почему куб повторной отправки выглядит лучше?

  1. ЦВЕТЕНИЕ — эффект свечения, исходящий от куба, называется цветением. В три js слишком сложно добавлять (example_1, example_2), и мне было просто лень. На самом деле я реализовал его однажды в другом проекте, которым, возможно, поделюсь в другой раз.
  2. Мини-кубы не одинаковы — ресенд имеет другую геометрию с «квадратами», которые выдавливаются в кубы с закругленными углами. Могло бы быть хорошим упражнением для вас и меня, чтобы попытаться сделать это в другой раз.
  3. Эффект вращения слоя — это выглядит круто, и теоретически его можно было бы получить, всегда вращая только 1 слой — прямо перед вращением вы можете перевернуть куб с помощью заранее определенного случайного преобразования, повернув его на одну из сторон. Таким образом, вы можете создать эффект, что вы всегда поворачиваете другую сторону.
  4. Улучшенная молния — прибить молнию сложно, поэтому я приблизился к ней как можно лучше (я потратил на это 2 минуты).

Я призываю вас добавить эти функции и отметить меня в результатах — возможно, в Твиттере. Веселиться.

Окончательный код:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Resend Cube</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three-addons.min.js"></script>
<script type="module">
    function createBoxWithRoundedEdges( width, height, depth, radius0, smoothness ) {
        let shape = new THREE.Shape();
        let eps = 0.00001;
        let radius = radius0 - eps;
        shape.absarc( eps, eps, eps, -Math.PI / 2, -Math.PI, true );
        shape.absarc( eps, height -  radius * 2, eps, Math.PI, Math.PI / 2, true );
        shape.absarc( width - radius * 2, height -  radius * 2, eps, Math.PI / 2, 0, true );
        shape.absarc( width - radius * 2, eps, eps, 0, -Math.PI / 2, true );
        let geometry = new THREE.ExtrudeBufferGeometry( shape, {
            depth: depth - radius0 * 2,
            bevelEnabled: true,
            bevelSegments: smoothness * 2,
            steps: 1,
            bevelSize: radius,
            bevelThickness: radius0,
            curveSegments: smoothness
        });
    geometry.center();
    return geometry;
    }
    function makeCubes() {
        const material = new THREE.MeshStandardMaterial({ color: 0x2a2a2a, metalness: 1, roughness: 0.11 });
        const numCubes = 3;
        // Create the group, we will add cubes to the group
        const cubes = new THREE.Group();
        // iterate over all dimensions
        for (let i = -Math.floor(numCubes / 2); i <= Math.floor(numCubes / 2); i++) {
            for (let j = -Math.floor(numCubes / 2); j <= Math.floor(numCubes / 2); j++) {
                for (let k = -Math.floor(numCubes / 2); k <= Math.floor(numCubes / 2); k++) {
                    // adding the cubes
                    const geom = createBoxWithRoundedEdges(1, 1, 1, .17, 20);
                    geom.translate(i, j, k);
                    const cube = new THREE.Mesh(geom, material);
                    cubes.add(cube);
                }
            }
        }
        return cubes;
    }
    // Create the scene
    const scene = new THREE.Scene();
    scene.background = new THREE.Color( 0x000000 );
    // Create the light
    const light = new THREE.PointLight( 0xffffff, 100, 500 );
    light.position.set( 0, 5, 10 );
    scene.add( light );
    const light2 = new THREE.PointLight( 0xffffff, 10, 500 );
    light2.position.set( 0, -10, -5 );
    scene.add( light2 );
    // Create the camera
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    // Create the renderer
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    // Create a cube
    const cube = makeCubes();
    scene.add(cube);
    // Position the camera
    camera.position.z = 10;
    const controls = new THREE_ADDONS.OrbitControls(camera,renderer.domElement);
    // Animate the cube
    function animate() {
        requestAnimationFrame(animate);
        cube.rotation.x += 0.005;
        cube.rotation.y -= 0.005;
        controls.update();
        renderer.render(scene, camera);
    }
    animate();
</script>
</body>
</html>