UNPKG

threed-garden

Version:

ThreeD Garden: WebGL 3D Environment Interface for Next.JS React TypeScript Three.JS React-Three Physics, 2D Paper.JS; APIs: Apollo GraphQL, WordPress; CSS: Tailwind, Radix-UI; Libraries: FarmBot 3D; AI: OpenAI, DeepSeek

424 lines (356 loc) 13.4 kB
import * as THREE from 'three'; import { ArcballControls } from 'three/addons/controls/ArcballControls.js'; import WebGL from 'three/addons/capabilities/WebGL.js'; import ThreeMeshUI from 'three-mesh-ui'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import GardenImage from "/image.jpeg"; // FarmBot dimensions const xAxisLength = 2800; const yAxisLength = 1300; const bedHeight = 300; // Set up and attach to DOM const scene = new THREE.Scene(); // Perspective camera // const camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 100, 10000 ); // Orthographic camera const camera = new THREE.OrthographicCamera( window.innerWidth / - 2 * 1000, window.innerWidth / 2 * 1000, window.innerHeight / 2 * 1000, window.innerHeight / - 2 * 1000, 100, 10000 ); // Camera position camera.position.set( 0, 0, 2750 ); camera.lookAt( 0, 0, 0 ); const renderer = new THREE.WebGLRenderer({ antialias: true }); // renderer.shadowMap.enabled = true; renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild( renderer.domElement ); const controls = new ArcballControls( camera, renderer.domElement, scene ); controls.cursorZoom = true; controls.enableAnimations = false; controls.maxDistance = 7500; // Perspective camera only controls.minDistance = 250; // Perspective camera only controls.maxZoom = 7500; // Orthographic camera only controls.minZoom = 250; // Orthographic camera only controls.setGizmosVisible( false ); controls.addEventListener( 'change', function () { renderer.render( scene, camera ); } ); // Ground const groundGeometry = new THREE.PlaneGeometry(10000, 10000); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0xf4f4f4 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.position.set(0, 0, -bedHeight); ground.receiveShadow = true; scene.add(ground); // Raised bed const bedGeometry = new THREE.BoxGeometry( xAxisLength + 100, yAxisLength + 100, bedHeight ); const bedMaterial = new THREE.MeshStandardMaterial( { color: 0xc39f7a } ); const bed = new THREE.Mesh( bedGeometry, bedMaterial ); bed.position.set(0, 0, -bedHeight / 2); bed.castShadow = true; bed.receiveShadow = true; scene.add( bed ); // Soil const soilGeometry = new THREE.BoxGeometry( xAxisLength, yAxisLength, bedHeight + 10 ); const soilMaterial = new THREE.MeshStandardMaterial( { color: 0x8e5e31 } ); const soil = new THREE.Mesh( soilGeometry, soilMaterial ); soil.position.set(0, 0, -bedHeight / 2); soil.receiveShadow = true; soil.castShadow = true; scene.add( soil ); // Plants const plantMaterial = new THREE.MeshStandardMaterial({ color: 0x66aa44, opacity: 0.8, transparent: true }); for (let i = 0; i < 150; i++) { // Create plant geometry with random diameter between 30 and 100 const plantGeometry = new THREE.SphereGeometry(Math.random() * (100 - 30) + 30); // Create plant const plant = new THREE.Mesh(plantGeometry, plantMaterial); // Set random position in bed plant.position.x = Math.random() * xAxisLength - xAxisLength / 2; plant.position.y = Math.random() * yAxisLength - yAxisLength / 2; plant.position.z = 0; // Add plant to scene plant.castShadow = true; plant.receiveShadow = true; scene.add(plant); } // Weeds const weedMaterial = new THREE.MeshStandardMaterial({ color: 0xee6666, opacity: 0.8, transparent: true }); for (let i = 0; i < 150; i++) { // Create weed geometry with random diameter between 10 and 50 const weedGeometry = new THREE.SphereGeometry(Math.random() * (50 - 10) + 10); // Create weed const weed = new THREE.Mesh(weedGeometry, weedMaterial); // Set random position in bed weed.position.x = Math.random() * xAxisLength - xAxisLength / 2; weed.position.y = Math.random() * yAxisLength - yAxisLength / 2; weed.position.z = 0; // Add weed to scene weed.castShadow = true; weed.receiveShadow = true; scene.add(weed); } // Grid lines function createGrid() { let material = new THREE.LineBasicMaterial({ color: 0x434343 }); let grid = new THREE.Group(); // X-axis lines for (let i = -yAxisLength / 2; i <= yAxisLength / 2; i += 100) { let hPoints = []; hPoints.push( new THREE.Vector3(-xAxisLength / 2, i, 10), new THREE.Vector3(xAxisLength / 2, i, 10)); let hGeometry = new THREE.BufferGeometry().setFromPoints( hPoints ); let hLine = new THREE.Line(hGeometry, material); grid.add(hLine); // Add labels const gridLabel = new ThreeMeshUI.Block({ width: 80, height: 40, padding: 10, borderRadius: 10, justifyContent: 'center', textAlign: 'center', fontFamily: '/Roboto-msdf.json', fontTexture: '/Roboto-msdf.png', fontColor: new THREE.Color(0xf4f4f4), backgroundOpacity: 0.5, }); const gridLabelText = new ThreeMeshUI.Text({ content: i.toFixed(0), fontSize: 20 }); gridLabel.add( gridLabelText ); gridLabel.position.set( -xAxisLength/2 - 50, i, 1 ); scene.add( gridLabel ); } // Y-axis lines for (let i = -xAxisLength / 2; i <= xAxisLength / 2; i += 100) { let vPoints = []; vPoints.push( new THREE.Vector3(i, -yAxisLength / 2, 10), new THREE.Vector3(i, yAxisLength / 2, 10)); let vGeometry = new THREE.BufferGeometry().setFromPoints( vPoints ); let vLine = new THREE.Line(vGeometry, material); grid.add(vLine); // Add labels const gridLabel = new ThreeMeshUI.Block({ width: 80, height: 40, padding: 10, borderRadius: 10, justifyContent: 'center', textAlign: 'center', fontFamily: '/Roboto-msdf.json', fontTexture: '/Roboto-msdf.png', fontColor: new THREE.Color(0xf4f4f4), backgroundOpacity: 0.5, }); const gridLabelText = new ThreeMeshUI.Text({ content: i.toFixed(0), fontSize: 20 }); gridLabel.add( gridLabelText ); gridLabel.position.set( i, -yAxisLength/2 - 25, 1 ); scene.add( gridLabel ); } scene.add(grid); } // Add text using https://felixmariotto.github.io/three-mesh-ui/ const textContainer = new ThreeMeshUI.Block({ width: 1200, height: 300, padding: 80, borderRadius: 25, justifyContent: 'center', textAlign: 'center', fontFamily: '/Roboto-msdf.json', fontTexture: '/Roboto-msdf.png', fontColor: new THREE.Color(0xf4f4f4), backgroundOpacity: 0.5, }); const text = new ThreeMeshUI.Text({ content: "FarmBot 3D Demo (click to toggle)", fontSize: 100 }); textContainer.add( text ); textContainer.position.set( 0, -yAxisLength/1.8, 200 ); textContainer.rotation.x = Math.PI / 8; scene.add( textContainer ); // Add photo using https://felixmariotto.github.io/three-mesh-ui/ const photoContainer = new ThreeMeshUI.Block({ height: 800, width: 600, }); new THREE.TextureLoader().load(GardenImage, (texture) => { photoContainer.set({ backgroundTexture: texture, }); }); photoContainer.position.set( 0, 0, 20 ); scene.add( photoContainer ); // FarmBot CAD let gantryColumnLeft, gantryColumnRight, gantryMainBeam; let farmBotLayer = new THREE.Group(); const loader = new GLTFLoader(); loader.load( '/gantry-column-left.gltf', function ( gltf ) { gltf.scene.traverse(function (node) { if (node instanceof THREE.Mesh) { node.castShadow = true; } }); gantryColumnLeft = gltf.scene; gantryColumnLeft.scale.set( 1000, 1000, 1000 ); gantryColumnLeft.rotation.z = Math.PI / 2; gantryColumnLeft.position.set( -1200, -yAxisLength / 2 - 50, 60 ); gantryColumnLeft.castShadow = true; farmBotLayer.add(gantryColumnLeft); scene.add(farmBotLayer); }); loader.load( '/gantry-column-right.gltf', function ( gltf ) { gltf.scene.traverse(function (node) { if (node instanceof THREE.Mesh) { node.castShadow = true; } }); gantryColumnRight = gltf.scene; gantryColumnRight.scale.set( 1000, 1000, 1000 ); gantryColumnRight.rotation.z = Math.PI / 2; gantryColumnRight.position.set( -1200, yAxisLength / 2 + 50, 60 ); gantryColumnRight.castShadow = true; farmBotLayer.add(gantryColumnRight); scene.add(farmBotLayer); }); loader.load( '/gantry-main-beam.gltf', function ( gltf ) { gltf.scene.traverse(function (node) { if (node instanceof THREE.Mesh) { node.castShadow = true; } }); gantryMainBeam = gltf.scene; gantryMainBeam.scale.set( 1000, 1000, 1000 ); gantryMainBeam.rotation.z = Math.PI / 2; gantryMainBeam.position.set( -1177.5, -750, 620 ); gantryMainBeam.castShadow = true; farmBotLayer.add(gantryMainBeam); scene.add(farmBotLayer); }); function toggleFarmBotLayer() { console.log("toggleFarmBotLayer"); if (farmBotLayer.visible) { farmBotLayer.visible = false; } else { farmBotLayer.visible = true; } } // Lighting (to illuminate the CAD model) var pointLight = new THREE.PointLight(0xFFFFFF, 1.25); pointLight.position.set(-4000, 3000, 4000); pointLight.castShadow = true; pointLight.shadow.mapSize.width = 1500; pointLight.shadow.mapSize.height = 1500; pointLight.shadow.camera.near = 0.1; pointLight.shadow.camera.far = 10000; scene.add(pointLight); var ambientLight = new THREE.AmbientLight(0x404040, 1); scene.add(ambientLight); // LED strip using spotlights // function createLEDStrip(start, end, numPoints, color, intensity) { // var points = []; // // // Create points along the line // for (var i = 0; i < numPoints; i++) { // var x = start.x + (end.x - start.x) * (i / (numPoints - 1)); // var y = start.y + (end.y - start.y) * (i / (numPoints - 1)); // var z = start.z + (end.z - start.z) * (i / (numPoints - 1)); // // // Create a spotlight at each point // var spotLight = new THREE.SpotLight(color, intensity); // spotLight.position.set(x, y, z); // // // Point the light downwards // spotLight.target.position.set(x, y, z - 1); // scene.add(spotLight.target); // // // Add the light to the scene // scene.add(spotLight); // }; // } // // var start = new THREE.Vector3(-1177.5, -700, 620); // var end = new THREE.Vector3(-1177.5, 700, 620); // createLEDStrip(start, end, 5, 0xffffee, 0.25); // LED strip using rect area light const rectLight = new THREE.RectAreaLight( 0xffffee, 100, 20, 1500 ); rectLight.position.set( -1177.5, 0, 560 ); rectLight.lookAt( -1177.5, 0, 0 ); scene.add( rectLight ) // Mouse raycasting const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); let rect; function updateRect() { rect = renderer.domElement.getBoundingClientRect(); } window.addEventListener('resize', updateRect); updateRect(); // Button hover effects window.addEventListener( 'mousemove', onMouseMove, false ); function onMouseMove( event ) { // calculate mouse position in normalized device coordinates // (-1 to +1) for both components mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // update the picking ray with the camera and mouse position raycaster.setFromCamera( mouse, camera ); // calculate objects intersecting the picking ray const intersects = raycaster.intersectObjects( [textContainer] ); if ( intersects.length > 0 ) { // Change the cursor to a pointer renderer.domElement.style.cursor = 'pointer'; // change the background color of the text container textContainer.set({ backgroundOpacity: 0.8 }); } else { // restore background opacity textContainer.set({ backgroundOpacity: 0.5 }); // Change the cursor back to default renderer.domElement.style.cursor = 'default'; } } // Button click action window.addEventListener( 'click', onClick, false ); function onClick( event ) { // calculate mouse position in normalized device coordinates // (-1 to +1) for both components mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // update the picking ray with the camera and mouse position raycaster.setFromCamera( mouse, camera ); // calculate objects intersecting the picking ray const intersects = raycaster.intersectObjects( [textContainer] ); if ( intersects.length > 0 ) { toggleFarmBotLayer() } } // Render loop function animate() { requestAnimationFrame( animate ); ThreeMeshUI.update(); // Pan camera back // camera.position.y -= 5; // Move "sun" // pointLight.position.x += 50; controls.update(); // update ThreeMeshUI components textContainer.updateMatrixWorld(); text.updateMatrixWorld(); renderer.render( scene, camera ); } // Compatibility check if ( WebGL.isWebGLAvailable() ) { createGrid(); animate(); } else { const warning = WebGL.getWebGLErrorMessage(); document.getElementById( 'container' ).appendChild( warning ); }