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
JSX
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 );
}