Посмотрите живую демонстрацию здесь: https://martisor.vercel.app/
Пройдя долгий и сложный путь изучения Three.js по учебникам Бруно Саймона, я с гордостью представляю плоды своего труда — мой первый проект на Three.js! Создание этой анимации было немалым подвигом, так почему бы не поделиться ею с другими, которые могут решить ту же задачу?
Чтобы подготовить почву, я начну этот рассказ с его вывода. Во-первых, вы увидите, что я построил, и, в конце концов, поймете мой первоначальный интерес к созданию сердца и лука с помощью Three.js.
Для тех, кто не знаком с Three.js
Ваш «Hello World» в Three.js
Прежде чем приступить к программированию, необходимо запустить сценарий Hello World, чтобы убедиться, что ваш код выполняется правильно и приводит к соответствующему результату. Лично для меня (в данном случае) это означает создание белого куба на черном холсте с помощью Three.js — действие, которое проверяет правильность положения моей камеры и масштаба куба для просмотра. Это простое упражнение служит подтверждением правильных настроек перед тем, как я перейду к более сложным проектам.
import * as THREE from "three"; // Scene const scene = new THREE.Scene(); // Canvas const canvas = document.querySelector("canvas.webgl"); // Cube const cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0xffffff })); scene.add(cube); // Sizes const sizes = { width: window.innerWidth, height: window.innerHeight, }; window.addEventListener("resize", () => { // Update sizes sizes.width = window.innerWidth; sizes.height = window.innerHeight; // Camera camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix(); // Update renderer renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); }); //Camera const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height); camera.position.z = 3; scene.add(camera); // Renderer const renderer = new THREE.WebGLRenderer({ canvas: canvas, }); renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.render(scene, camera); // If you are new to Three.js, try and understand what do you need each row from this code. // windows.addEventListener will help you see the cube even if you minimize your page.
Примечание. Документ HTML и CSS можно найти в моем репозитории GitHub.
Теперь давайте придадим этому блоку больше гибкости, введя контроль, чтобы вы могли наблюдать форму, выходящую за рамки стандартного квадрата:
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; //Camera // ... // Controls const controls = new OrbitControls(camera, canvas); controls.enableDamping = true; // Renderer // ... // Animate const tick = () => { // Update controls controls.update(); // Render renderer.render(scene, camera); // Call tick again on the next frame window.requestAnimationFrame(tick); }; tick(); // You can play with it: change the cube's color, size // Or you can change the camera position just to see how the things work in Three.js
Добавление графического интерфейса
Кроме того, мы можем повеселиться с кубом прямо с главной страницы.
Добавление графического интерфейса может помочь сделать ваше приложение Three.js более интерактивным, удобным для пользователя и визуально привлекательным. Это также может упростить процесс отладки и тестирования вашего приложения, предоставляя простой способ просмотра и изменения различных свойств объектов в сцене.
Я использовал lil-gui для своего проекта, но вы можете изучить и другие альтернативы.
import * as dat from "lil-gui"; // Add GUI const gui = new dat.GUI(); // Cube // ... // Add debugging after you created the cube gui.add(cube.position, "x").name("cubeX").min(-20).max(20).step(0.0001); gui.add(cube.position, "y").name("cubeY").min(-20).max(20).step(0.0001); gui.add(cube.position, "z").name("cubeZ").min(-20).max(20).step(0.0001); // Now you can play with the cube from the website page
На данный момент, имея рабочий куб и графический интерфейс, я чувствую, что моя среда готова для создания искусства.
Начнем с формы сердца.
Сначала я был убежден, что формировать форму сердца для меня слишком сложно. Однако это оказалось намного проще, чем ожидалось, при использовании документации Three.js, которая содержала все необходимые детали и инструкции о том, как его создать.
Обновите свой скрипт, заменив куб Сердцем.
Не забудьте удалить поле из графического интерфейса в это время. Мы можем персонализировать его впоследствии для нашего сердечного объекта.
// Heart Object const heartX = -25; const heartY = -25; const heartShape = new THREE.Shape(); heartShape.moveTo(25 + heartX, 25 + heartY); heartShape.bezierCurveTo(25 + heartX, 25 + heartY, 20 + heartX, 0 + heartY, 0 + heartX, 0 + heartY); heartShape.bezierCurveTo(-30 + heartX, 0 + heartY, -30 + heartX, 35 + heartY, -30 + heartX, 35 + heartY); heartShape.bezierCurveTo(-30 + heartX, 55 + heartY, -10 + heartX, 77 + heartY, 25 + heartX, 95 + heartY); heartShape.bezierCurveTo(60 + heartX, 77 + heartY, 80 + heartX, 55 + heartY, 80 + heartX, 35 + heartY); heartShape.bezierCurveTo(80 + heartX, 35 + heartY, 80 + heartX, 0 + heartY, 50 + heartX, 0 + heartY); heartShape.bezierCurveTo(35 + heartX, 0 + heartY, 25 + heartX, 25 + heartY, 25 + heartX, 25 + heartY); const extrudeSettings = { depth: 8, bevelEnabled: true, bevelSegments: 2, steps: 2, bevelSize: 1, bevelThickness: 1, }; const materialRed = new THREE.MeshBasicMaterial({ color: 0xffffff, }); const geometryHeart = new THREE.ExtrudeGeometry(heartShape, extrudeSettings); const meshHeart = new THREE.Mesh(geometryHeart, materialRed); meshHeart.scale.set(0.01, 0.01, 0.01); scene.add(meshHeart);
Я скорректировал объект и отцентрировал его, добавив heartX и heartY к исходной формуле.
Если вы хотите повернуть его, просто добавьте:
meshHeart.rotation.set(0, 0, Math.PI); // Or you can play with it and customize it's rotation settings. meshHeart.rotation.set(0, 0, Math.PI - Math.PI / 8); // Now you can adjust GUI for heartObject gui.add(meshHeart.position, "x").name("X").min(-20).max(20).step(0.0001); gui.add(meshHeart.position, "y").name("Y").min(-20).max(20).step(0.0001); gui.add(meshHeart.position, "z").name("Z").min(-20).max(20).step(0.0001); // Also, you can play with rotation settings gui.add(meshHeart.rotation, "x").name("rotateX").min(-20).max(20).step(0.0001); gui.add(meshHeart.rotation, "y").name("rotateY").min(-20).max(20).step(0.0001); gui.add(meshHeart.rotation, "z").name("rotateX").min(-20).max(20).step(0.0001);
Создайте форму лука
Несмотря на все мои усилия, мне не удалось найти в Интернете формулу построения формы банта (подобной той, что используется в качестве украшения). К счастью, поскольку луки по своей природе очень симметричны, вскоре я понял, как сделать их самостоятельно.
Сначала я разделил лук на несколько частей:
Начнем с петель лука
// Define the control points of the curve that defines the shape of the loop const curve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0.5, 0), new THREE.Vector3(1.5, 0, 0), new THREE.Vector3(1, -0.5, 0), new THREE.Vector3(0, 0, 0), ]); // Define the radius and number of segments of the tube geometry const radius = 0.01; const segments = 64; // Create a tube geometry along the curve with the specified radius and segments const loopGeometryLeft = new THREE.TubeGeometry(curve, segments, radius); // Create a mesh from the tube geometry with a material const bowMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff }); const loopMeshLeft = new THREE.Mesh(loopGeometryLeft, bowMaterial); scene.add(loopMeshLeft);
С контрольными точками кривой вы можете настроить свою петлю так, чтобы она была тугой или свободной по желанию. Проявите творческий подход и создайте идеальную петлю для любого применения!
Для создания зеркальной петли у нас есть два варианта — либо настроить координаты вектора по оси X вручную, либо придумать для этого функцию.
// Mirror the curve by negating the x-coordinates of its control points for the left bow const mirroredPoints = []; curve.points.forEach(function (point) { mirroredPoints.push(new THREE.Vector3(-point.x, point.y, point.z)); }); const mirroredCurve = new THREE.CatmullRomCurve3(mirroredPoints); // Create a tube geometry along the mirrored curve with the specified radius and segments const loopGeometryRight = new THREE.TubeGeometry(mirroredCurve, segments, radius); // Create a mesh from the mirrored tube geometry with a material const loopMeshRight = new THREE.Mesh(loopGeometryRight, bowMaterial); scene.add(loopMeshRight);
Фантастическая работа! (про себя говорю). Я уверен, что вы уже запустили свою программу Three.js, чтобы увидеть потрясающий результат, и она работает без сбоев. В случае возникновения каких-либо проблем, пожалуйста, оставьте мне сообщение, чтобы вместе мы могли решить эту проблему.
Перейдем к лентам.
const ribbonCurve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, -1.5, 0), // new THREE.Vector3(1, -1.7, 0), ]); // Create tube geometries along the ribbon curves with the specified radius and segments const ribbonGeometryLeft = new THREE.TubeGeometry(ribbonCurve, segments, radius * 0.5); // Create meshes from the ribbon geometries with the bow material const ribbonMeshLeft = new THREE.Mesh(ribbonGeometryLeft, bowMaterial); scene.add(ribbonMeshLeft);
Вы также можете настроить ленту. Удалив комментарии из третьей пары векторов, вы можете добавить своей ленте уникальный «хвост». Кроме того, попробуйте добавить еще один вектор в качестве индивидуального штриха, чтобы придать ему более индивидуальный вид!
Для левой ленты применим аналогичное обоснование — разработаем алгоритм, который будет инвертировать координаты по оси X.
// Mirror the curve by negating the x-coordinates of its control points const mirroredRibbonCurve = []; ribbonCurve.points.forEach(function (point) { mirroredRibbonCurve.push(new THREE.Vector3(-point.x, point.y, point.z)); }); const mirroredRibbon = new THREE.CatmullRomCurve3(mirroredRibbonCurve); // Create tube geometries along the ribbon curves with the specified radius and segments const ribbonGeometryRight = new THREE.TubeGeometry(mirroredRibbon, segments, radius * 0.5); // Create meshes from the ribbon geometries with the bow material const ribbonMeshRight = new THREE.Mesh(ribbonGeometryRight, bowMaterial); scene.add(ribbonMeshRight);
Все, что требуется сейчас, — это сформировать группу BowGroup путем слияния loopMeshLeft, loopMeshRight, ленточного MeshLeft и ленточного MeshRight.
const bowGroup = new THREE.Group(); // Substituite in your code where you have scene.add() with bowGroup.add() // The at the end you can add your group to the scene scene.add(bowGroup);
Вуаля!
Я надеюсь, что вы дошли до этого момента без каких-либо ошибок и что ваш конечный продукт — это 3D-лук и 3D-сердце, созданные с помощью Three.js!
А Мэрцишор?
Теперь позвольте мне представить значение Мэрцишор.
Мэрцишор — традиционный румынский праздник, отмечаемый 1 марта. Слово «мэрцишор» происходит от румынского слова «martie», что означает март. Этот праздник связан с приходом весны и отмечается вручением и получением небольших подарков, обычно в виде плетеных красных и белых ниток.
Нить Мэрцишор традиционно изготавливается из двух переплетенных нитей, белой и красной, которые символизируют приход весны и обновление жизни. Нить обычно носят в виде броши, прикалывают к одежде или завязывают на запястье и носят весь март.
Этой весной ничто так не говорит «Я люблю тебя», как Мэрцишор, сделанный из двух сердечек и бантика белого и красного цветов. Символизируя единство, это идеальный способ выразить свои чувства.
Также в конце марта нить Мэрцишор обычно снимают и привязывают к ветке плодового или цветущего дерева, например вишни, яблони или груши. Это делается в надежде, что дерево будет благословлено плодородным годом и что владелец Мэрцишора будет здоровым и сильным в течение всего года.
Чтобы объединить все объекты, созданные выше, я создал еще одну группу специально для Мэрцишора.
import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import * as dat from "lil-gui"; // Debug const gui = new dat.GUI(); // Scene const scene = new THREE.Scene(); // Canvas const canvas = document.querySelector("canvas.webgl"); // Heart group const heartGroup = new THREE.Group(); // Heart Object const heartX = -25; const heartY = -25; const heartShape = new THREE.Shape(); heartShape.moveTo(25 + heartX, 25 + heartY); heartShape.bezierCurveTo(25 + heartX, 25 + heartY, 20 + heartX, 0 + heartY, 0 + heartX, 0 + heartY); heartShape.bezierCurveTo(-30 + heartX, 0 + heartY, -30 + heartX, 35 + heartY, -30 + heartX, 35 + heartY); heartShape.bezierCurveTo(-30 + heartX, 55 + heartY, -10 + heartX, 77 + heartY, 25 + heartX, 95 + heartY); heartShape.bezierCurveTo(60 + heartX, 77 + heartY, 80 + heartX, 55 + heartY, 80 + heartX, 35 + heartY); heartShape.bezierCurveTo(80 + heartX, 35 + heartY, 80 + heartX, 0 + heartY, 50 + heartX, 0 + heartY); heartShape.bezierCurveTo(35 + heartX, 0 + heartY, 25 + heartX, 25 + heartY, 25 + heartX, 25 + heartY); const extrudeSettings = { depth: 8, bevelEnabled: true, bevelSegments: 2, steps: 2, bevelSize: 1, bevelThickness: 1, }; const white = "#ffffff"; const red = "#ff0000"; const redMaterial = new THREE.MeshBasicMaterial({ color: red, }); const whiteMaterial = new THREE.MeshBasicMaterial({ color: white, }); const heartGeometryRight = new THREE.ExtrudeGeometry(heartShape, extrudeSettings); const hearMeshRight = new THREE.Mesh(heartGeometryRight, redMaterial); const heartGeometryLeft = new THREE.ExtrudeGeometry(heartShape, extrudeSettings); const hearMeshLeft = new THREE.Mesh(heartGeometryLeft, whiteMaterial); hearMeshRight.scale.set(0.01, 0.01, 0.01); hearMeshRight.rotation.set(0, 0, Math.PI - Math.PI / 8); hearMeshLeft.scale.set(0.01, 0.01, 0.01); hearMeshLeft.position.set(-1.66, -0.35, 0); hearMeshLeft.rotation.set(0, 0, Math.PI + Math.PI / 7); heartGroup.add(hearMeshRight, hearMeshLeft); heartGroup.position.set(1, -1.7, 0); scene.add(heartGroup); // Gui gui.add(heartGroup.position, "x").min(-10).max(10).step(0.01).name("heartX"); gui.add(heartGroup.position, "y").min(-10).max(10).step(0.01).name("heartY"); gui.add(heartGroup.position, "z").min(-10).max(10).step(0.01).name("heartZ"); // Bow group const bowGroup = new THREE.Group(); // Define the control points of the curve that defines the shape of the bow const loopCurve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0.5, 0), new THREE.Vector3(1.5, 0, 0), new THREE.Vector3(1, -0.5, 0), new THREE.Vector3(0, 0, 0), ]); // Define the radius and number of segments of the tube geometry const radius = 0.03; const segments = 64; // Create a tube geometry along the curve with the specified radius and segments const loopGeometryRight = new THREE.TubeGeometry(loopCurve, segments, radius); // Create a mesh from the tube geometry with a material const bowMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const meshLoopRight = new THREE.Mesh(loopGeometryRight, bowMaterial); // Mirror the curve by negating the x-coordinates of its control points for the left bow const mirroredPoints = []; loopCurve.points.forEach(function (point) { mirroredPoints.push(new THREE.Vector3(-point.x, point.y, point.z)); }); const mirroredCurve = new THREE.CatmullRomCurve3(mirroredPoints); // Create a tube geometry along the mirrored curve with the specified radius and segments const loopGeometryLeft = new THREE.TubeGeometry(mirroredCurve, segments, radius); // Create a mesh from the mirrored tube geometry with a material const meshLoopLeft = new THREE.Mesh(loopGeometryLeft, bowMaterial); const ribbonCurve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, -1.5, 0), new THREE.Vector3(1, -1.7, 0), // new THREE.Vector3(2 * bowx, -0.2 * bowx, 0), ]); // Mirror the curve by negating the x-coordinates of its control points const mirroredRibbonCurve = []; ribbonCurve.points.forEach(function (point) { if (point.x != 0) { mirroredRibbonCurve.push(new THREE.Vector3(-point.x + 0.3, point.y - 0.3, point.z)); } else { mirroredRibbonCurve.push(new THREE.Vector3(point.x, point.y, point.z)); } }); const mirroredRibbon = new THREE.CatmullRomCurve3(mirroredRibbonCurve); // Create tube geometries along the ribbon curves with the specified radius and segments const ribbonGeometryRight = new THREE.TubeGeometry(ribbonCurve, segments, radius * 0.5); const ribbonGeometryLeft = new THREE.TubeGeometry(mirroredRibbon, segments, radius * 0.5); // Create meshes from the ribbon geometries with the bow material const ribbonMeshRight = new THREE.Mesh(ribbonGeometryRight, bowMaterial); const ribbonMeshLeft = new THREE.Mesh(ribbonGeometryLeft, bowMaterial); bowGroup.add(meshLoopRight); bowGroup.add(meshLoopLeft); bowGroup.add(ribbonMeshLeft); bowGroup.add(ribbonMeshRight); scene.add(bowGroup); // Sizes const sizes = { width: window.innerWidth, height: window.innerHeight, }; window.addEventListener("resize", () => { // Update sizes sizes.width = window.innerWidth; sizes.height = window.innerHeight; // Update camera camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix(); // Update renderer renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); }); // Camera const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100); camera.position.z = 3; scene.add(camera); // Controls const controls = new OrbitControls(camera, canvas); controls.enableDamping = true; // Renderer const renderer = new THREE.WebGLRenderer({ canvas: canvas, }); renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Animate const tick = () => { // Update controls controls.update(); // Render renderer.render(scene, camera); // Call tick again on the next frame window.requestAnimationFrame(tick); }; tick();
Вместо традиционного объектно-ориентированного программирования я решил применить функциональный подход при рефакторинге своего кода.
import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import * as dat from "lil-gui"; const createHeart = (heartShape, material, extrudeSettings, isLeft, scale) => { let geometry = new THREE.ExtrudeGeometry(heartShape, extrudeSettings); let mesh = new THREE.Mesh(geometry, material); if (isLeft) { mesh.position.set(-1.67, -0.35, 0); mesh.rotation.set(0, 0, Math.PI + Math.PI / 7); } else { mesh.position.set(0, 0, 0); mesh.rotation.set(0, 0, Math.PI - Math.PI / 8); } mesh.scale.set(scale, scale, scale); return mesh; }; const createBow = (loopCurve, ribbonCurve, material, radius, segments) => { const bowGroup = new THREE.Group(); // Create a tube geometry along the curve with the specified radius and segments const loopGeometryRight = new THREE.TubeGeometry(loopCurve, segments, radius); // Create a mesh from the tube geometry with a material const loopMeshRight = new THREE.Mesh(loopGeometryRight, material); // Mirror the curve by negating the x-coordinates of its control points for the left bow const mirroredPoints = []; loopCurve.points.forEach((point) => { mirroredPoints.push(new THREE.Vector3(-point.x, point.y, point.z)); }); const mirroredCurve = new THREE.CatmullRomCurve3(mirroredPoints); // Create a tube geometry along the mirrored curve with the specified radius and segments const loopGeometryLeft = new THREE.TubeGeometry(mirroredCurve, segments, radius); // Create a mesh from the mirrored tube geometry with a material const loopMeshLeft = new THREE.Mesh(loopGeometryLeft, material); // Mirror the curve by negating the x-coordinates of its control points const mirroredRibbonCurve = []; ribbonCurve.points.forEach((point) => { if (point.x != 0) { // Here I am trying to move the points a bit, so that the left ribbon is quite different from the right one mirroredRibbonCurve.push(new THREE.Vector3(-point.x + 0.3, point.y - 0.3, point.z)); } else { mirroredRibbonCurve.push(new THREE.Vector3(point.x, point.y, point.z)); } }); const mirroredRibbon = new THREE.CatmullRomCurve3(mirroredRibbonCurve); // Create tube geometries along the ribbon curves with the specified radius and segments const ribbonGeometryRight = new THREE.TubeGeometry(ribbonCurve, segments, radius * 0.5); const ribbonGeometryLeft = new THREE.TubeGeometry(mirroredRibbon, segments, radius * 0.5); // Create meshes from the ribbon geometries with the bow material const ribbonMeshRight = new THREE.Mesh(ribbonGeometryRight, material); const ribbonMeshLeft = new THREE.Mesh(ribbonGeometryLeft, material); bowGroup.add(loopMeshRight); bowGroup.add(loopMeshLeft); bowGroup.add(ribbonMeshRight); bowGroup.add(ribbonMeshLeft); return bowGroup; }; const createMartisor = (bow, heartLeft, heartRight) => { const heartGroup = new THREE.Group(); heartGroup.add(heartLeft); heartGroup.add(heartRight); heartGroup.position.set(1, -1.65, -0.02); const martisorGroup = new THREE.Group(); martisorGroup.add(bow); martisorGroup.add(heartGroup); martisorGroup.position.set(0, 0, 0); return martisorGroup; }; // Animate const tick = (controls, renderer, scene, camera) => { // Update controls controls.update(); // Render renderer.render(scene, camera); // Call tick again on the next frame window.requestAnimationFrame(() => tick(controls, renderer, scene, camera)); }; const main = () => { // Debug const gui = new dat.GUI(); // Scene const scene = new THREE.Scene(); // Canvas const canvas = document.querySelector("canvas.webgl"); // Materials and other variables const white = "#ffffff"; const red = "#ff0000"; const redMaterial = new THREE.MeshBasicMaterial({ color: red, }); const whiteMaterial = new THREE.MeshBasicMaterial({ color: white, }); // Heart Object const heartX = -25; const heartY = -25; const heartShape = new THREE.Shape(); heartShape.moveTo(25 + heartX, 25 + heartY); heartShape.bezierCurveTo(25 + heartX, 25 + heartY, 20 + heartX, 0 + heartY, 0 + heartX, 0 + heartY); heartShape.bezierCurveTo(-30 + heartX, 0 + heartY, -30 + heartX, 35 + heartY, -30 + heartX, 35 + heartY); heartShape.bezierCurveTo(-30 + heartX, 55 + heartY, -10 + heartX, 77 + heartY, 25 + heartX, 95 + heartY); heartShape.bezierCurveTo(60 + heartX, 77 + heartY, 80 + heartX, 55 + heartY, 80 + heartX, 35 + heartY); heartShape.bezierCurveTo(80 + heartX, 35 + heartY, 80 + heartX, 0 + heartY, 50 + heartX, 0 + heartY); heartShape.bezierCurveTo(35 + heartX, 0 + heartY, 25 + heartX, 25 + heartY, 25 + heartX, 25 + heartY); const extrudeSettings = { depth: 8, bevelEnabled: true, bevelSegments: 2, steps: 2, bevelSize: 1, bevelThickness: 1, }; const heartRight = createHeart(heartShape, redMaterial, extrudeSettings, false, 0.01); const heartLeft = createHeart(heartShape, whiteMaterial, extrudeSettings, true, 0.01); // Define the control points of the curve that defines the shape of the bow const loopCurve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0.5, 0), new THREE.Vector3(1.5, 0, 0), new THREE.Vector3(1, -0.5, 0), new THREE.Vector3(0, 0, 0), ]); // Define the radius and number of segments of the tube geometry const radius = 0.03; const segments = 64; // Create a mesh from the tube geometry with a material const materialBow = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const ribbonCurve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, -1.5, 0), new THREE.Vector3(1, -1.7, 0), // new THREE.Vector3(2 * bowx, -0.2 * bowx, 0), ]); const bowGroup = createBow(loopCurve, ribbonCurve, materialBow, radius, segments); // scene.add(bowGroup); const martisor = createMartisor(bowGroup, heartRight, heartLeft); scene.add(martisor); // Sizes const sizes = { width: window.innerWidth, height: window.innerHeight, }; window.addEventListener("resize", () => { // Update sizes sizes.width = window.innerWidth; sizes.height = window.innerHeight; // Update camera camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix(); // Update renderer renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); }); // Camera const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100); camera.position.z = 3; scene.add(camera); // Controls const controls = new OrbitControls(camera, canvas); controls.enableDamping = true; // Renderer const renderer = new THREE.WebGLRenderer({ canvas: canvas, }); renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); tick(controls, renderer, scene, camera); }; main();
В конце концов, я создал множество групп martisor (martisoare — форма множественного числа на румынском языке) и позволил им уплыть в небытие.
const spawnMartisoare = (martisor, scene, count) => { const martisoare = []; for (let i = 0; i < count * 3; i++) { const martisorClone = martisor.clone(); let x = (Math.random() - 0.5) * 100; let y = (Math.random() - 0.5) * 100; let z = (Math.random() - 0.5) * 50; martisorClone.position.set(x, y, z); martisorClone.rotation.set(0, (Math.random() * Math.PI) / 6, (Math.random() * Math.PI) / 4); scene.add(martisorClone); martisoare.push({ shape: martisorClone, x: Math.random(), y: Math.random(), z: Math.random(), }); } return martisoare; };
Чтобы запустить анимацию, вам также необходимо изменить функцию тика.
const tick = (controls, renderer, scene, camera, martisoare) => { // Update controls controls.update(); // Update martisoare const speed = 0.01; martisoare.forEach((el) => { el.shape.rotation.x += el.x * speed; el.shape.rotation.y += el.y * 1.5 * speed; el.shape.rotation.z += el.z * 2.5 * speed; }); // Render renderer.render(scene, camera); // Call tick again on the next frame window.requestAnimationFrame(() => tick(controls, renderer, scene, camera, martisoare)); };
Тада-а-а-а-а-ам! 🎇 Я очень рад возможности поделиться с вами конечным продуктом; Я изменил цвет фона и вдумчиво создал различные оттенки красного для ваших любимых сердечек. Вот! Все эти удивительные объекты плавают в гармонии — до нашего следующего совместного путешествия!
❤️ https://martisor.vercel.app/
🤍 https://github.com/Welnic/martisor
После того, как я завершил этот проект, стало ясно, что весь процесс был чудесным путешествием и невероятно полезным усилием. Поскольку это моя первая работа, будьте осторожны с критикой, если вы знакомы с Three.js; тем не менее, я был бы рад получить любые советы или подсказки о том, как внести улучшения! Кроме того, если вам понравилось то, что вы здесь увидели, не стесняйтесь, дайте мне знать о ваших идеях для новых дополнений — я всегда ищу возможности улучшить свои навыки анимации.
P.S. Я создал этот 3D-мэрцишор с любовью для кого-то особенного для меня, и теперь вы тоже можете украсить день человека из Румынии, подарив ему свой собственный мраморный мрамор — трехмерный или сделанный вручную! Вы точно произведете впечатление! 🤍❤️