import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const canvas = document.getElementById('farm-canvas'); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false }); renderer.setSize(window.innerWidth * 0.65, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; renderer.outputColorSpace = THREE.SRGBColorSpace; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(50, (window.innerWidth * 0.65) / window.innerHeight, 0.1, 800); camera.position.set(0, 18, 48); camera.lookAt(0, 0, 0); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.06; controls.minDistance = 6; controls.maxDistance = 50; controls.maxPolarAngle = Math.PI / 2.1; controls.target.set(0, 0, 0); const skyHemi = new THREE.HemisphereLight(0x87ceeb, 0xc8a030, 0.7); scene.add(skyHemi); const sunLight = new THREE.DirectionalLight(0xffeedd, 2.5); sunLight.position.set(20, 40, 15); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 1024; sunLight.shadow.mapSize.height = 1024; sunLight.shadow.camera.near = 0.1; sunLight.shadow.camera.far = 200; sunLight.shadow.camera.left = -30; sunLight.shadow.camera.right = 30; sunLight.shadow.camera.top = 30; sunLight.shadow.camera.bottom = -30; scene.add(sunLight); const ambientFill = new THREE.AmbientLight(0x304060, 0.3); scene.add(ambientFill); const skyGeo = new THREE.SphereGeometry(300, 16, 8); const skyMat = new THREE.ShaderMaterial({ side: THREE.BackSide, uniforms: { topColor: { value: new THREE.Color(0x5ca2e8) }, bottomColor: { value: new THREE.Color(0xdcecf8) }, offset: { value: 30 }, exponent: { value: 0.6 }, }, vertexShader: ` varying vec3 vWorldPosition; void main() { vec4 wp = modelMatrix * vec4(position, 1.0); vWorldPosition = wp.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 topColor; uniform vec3 bottomColor; uniform float offset; uniform float exponent; varying vec3 vWorldPosition; void main() { float h = normalize(vWorldPosition + offset).y; gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h,0.0), exponent), 0.0)), 1.0); } `, }); scene.add(new THREE.Mesh(skyGeo, skyMat)); const groundGeo = new THREE.PlaneGeometry(160, 160, 120, 120); const soilMat = new THREE.ShaderMaterial({ uniforms: { soilMoisture: { value: 0.5 }, soilHealth: { value: 0.7 }, time: { value: 0 }, }, vertexShader: ` uniform float time; varying vec2 vUv; void main() { vUv = uv; vec3 p = position; p.z += sin(p.x * 1.795) * 0.35 + sin(p.x * 0.5 + time * 0.05) * 0.05; gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); } `, fragmentShader: ` uniform soilMoisture; uniform soilHealth; varying vec2 vUv; void main() { vec3 dry = vec3(0.35, 0.22, 0.12); vec3 wet = vec3(0.18, 0.11, 0.05); vec3 dead = vec3(0.28, 0.23, 0.18); vec3 c = mix(dry, wet, soilMoisture); c = mix(dead, c, soilHealth); c += (fract(sin(dot(vUv*50.0, vec2(127.1,311.7)))*43758.5) - 0.5) * 0.04; gl_FragColor = vec4(c, 1.0); } `, }); const ground = new THREE.Mesh(groundGeo, soilMat); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); const ROWS = 12; const COLS = 16; const cropMeshes = []; const stemGeo = new THREE.CylinderGeometry(0.03, 0.05, 1, 5); const leafGeo = new THREE.PlaneGeometry(0.4, 0.22); const bollGeo = new THREE.SphereGeometry(0.1, 5, 5); let currentCropPhase = 0; function buildCropField(phase) { cropMeshes.forEach(g => scene.remove(g)); cropMeshes.length = 0; const stemScale = phase === 0 ? 0.7 : phase === 1 ? 1.4 : 1.9; const leafCount = phase === 0 ? 2 : phase === 1 ? 4 : 5; const healthHue = phase === 2 ? 0.22 : 0.28; const showBolls = phase === 2; for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { const group = new THREE.Group(); const sMat = new THREE.MeshLambertMaterial({ color: new THREE.Color().setHSL(0.25, 0.65, 0.22 + phase * 0.08) }); const stem = new THREE.Mesh(stemGeo, sMat); stem.scale.y = stemScale; stem.position.y = stemScale * 0.5; stem.castShadow = true; group.add(stem); for (let l = 0; l < leafCount; l++) { const lMat = new THREE.MeshLambertMaterial({ color: new THREE.Color().setHSL(healthHue, 0.7, 0.25 + phase * 0.05), side: THREE.DoubleSide, transparent: true, opacity: 0.85, }); const leaf = new THREE.Mesh(leafGeo, lMat); const angle = (l / leafCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.3; leaf.position.set( Math.cos(angle) * (0.15 + phase * 0.06), stemScale * 0.45 + l * 0.1, Math.sin(angle) * (0.15 + phase * 0.06) ); leaf.rotation.set(0.2, angle, 0.12); leaf.castShadow = true; group.add(leaf); } if (showBolls) { for (let b = 0; b < 3; b++) { const bMat = new THREE.MeshLambertMaterial({ color: 0xf5f0e0 }); const boll = new THREE.Mesh(bollGeo, bMat); boll.position.set( (Math.random() - 0.5) * 0.25, stemScale * 0.7 + b * 0.08, (Math.random() - 0.5) * 0.25 ); group.add(boll); } } group.position.set( (col - COLS / 2) * 3.5 + 1.75 + (Math.random() - 0.5) * 0.3, 0, (row - ROWS / 2) * 3.2 + 1.6 + (Math.random() - 0.5) * 0.3 ); scene.add(group); cropMeshes.push(group); } } currentCropPhase = phase; } buildCropField(0); const fenceGroup = new THREE.Group(); const postGeo = new THREE.BoxGeometry(0.4, 3.5, 0.4); const woodMat = new THREE.MeshLambertMaterial({ color: 0x4a3b2c }); const beamGeoX = new THREE.BoxGeometry(90, 0.25, 0.15); const beamGeoZ = new THREE.BoxGeometry(0.15, 0.25, 90); for(let z=-45; z<=45; z+=9) { const p = new THREE.Mesh(postGeo, woodMat); p.position.set(-45, 1.75, z); p.castShadow = true; fenceGroup.add(p); } const beamL1 = new THREE.Mesh(beamGeoZ, woodMat); beamL1.position.set(-45, 2.6, 0); beamL1.castShadow = true; fenceGroup.add(beamL1); const beamL2 = new THREE.Mesh(beamGeoZ, woodMat); beamL2.position.set(-45, 1.3, 0); beamL2.castShadow = true; fenceGroup.add(beamL2); for(let z=-45; z<=45; z+=9) { const p = new THREE.Mesh(postGeo, woodMat); p.position.set(45, 1.75, z); p.castShadow = true; fenceGroup.add(p); } const beamR1 = new THREE.Mesh(beamGeoZ, woodMat); beamR1.position.set(45, 2.6, 0); beamR1.castShadow = true; fenceGroup.add(beamR1); const beamR2 = new THREE.Mesh(beamGeoZ, woodMat); beamR2.position.set(45, 1.3, 0); beamR2.castShadow = true; fenceGroup.add(beamR2); for(let x=-45; x<=45; x+=9) { const p = new THREE.Mesh(postGeo, woodMat); p.position.set(x, 1.75, -45); p.castShadow = true; fenceGroup.add(p); } const beamB1 = new THREE.Mesh(beamGeoX, woodMat); beamB1.position.set(0, 2.6, -45); beamB1.castShadow = true; fenceGroup.add(beamB1); const beamB2 = new THREE.Mesh(beamGeoX, woodMat); beamB2.position.set(0, 1.3, -45); beamB2.castShadow = true; fenceGroup.add(beamB2); scene.add(fenceGroup); [[-95, 0, 0], [95, 0, 0], [0, 0, -95]].forEach(([x, y, z]) => { const fm = new THREE.Mesh( new THREE.PlaneGeometry(100, 100), new THREE.MeshLambertMaterial({ color: 0x244f24 }) ); fm.rotation.x = -Math.PI / 2; fm.position.set(x, 0.05, z); fm.receiveShadow = true; scene.add(fm); }); let rainParticles = null; let hazeParticles = null; function clearWeatherEffects() { if (rainParticles) { scene.remove(rainParticles); rainParticles = null; } if (hazeParticles) { scene.remove(hazeParticles); hazeParticles = null; } } function createRain(intensity) { clearWeatherEffects(); const count = Math.floor(800 * intensity); const geo = new THREE.BufferGeometry(); const pos = new Float32Array(count * 3); const vel = new Float32Array(count * 3); for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5) * 60; pos[i*3+1] = Math.random() * 25 + 5; pos[i*3+2] = (Math.random()-0.5) * 60; vel[i*3] = (Math.random()-0.5) * 0.4; vel[i*3+1] = -(2.5 + Math.random()*2) * intensity; vel[i*3+2] = (Math.random()-0.5) * 0.4; } geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setAttribute('velocity', new THREE.BufferAttribute(vel, 3)); const mat = new THREE.PointsMaterial({ color: 0xaaccff, size: 0.04, transparent: true, opacity: 0.55 }); rainParticles = new THREE.Points(geo, mat); scene.add(rainParticles); } function createDroughtHaze() { clearWeatherEffects(); const count = 350; const geo = new THREE.BufferGeometry(); const pos = new Float32Array(count * 3); for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5) * 80; pos[i*3+1] = Math.random() * 7; pos[i*3+2] = (Math.random()-0.5) * 80; } geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const mat = new THREE.PointsMaterial({ color: 0xc8a882, size: 0.22, transparent: true, opacity: 0.25 }); hazeParticles = new THREE.Points(geo, mat); scene.add(hazeParticles); } function setDayTime(dayFrac) { const hour = 8 + dayFrac * 10; const angle = (hour / 24) * Math.PI * 2; sunLight.position.set(Math.cos(angle)*100, Math.abs(Math.sin(angle))*80 + 5, 25); sunLight.intensity = 1.5 + Math.sin(angle) * 1.2; const warmth = 0.3 + (Math.sin(angle)*0.5+0.5) * 0.2; sunLight.color.setHSL(0.07, 0.9, warmth + 0.3); } export function updateFromFarmState(farmState, weatherCondition) { soilMat.uniforms.soilMoisture.value = farmState.soil_moisture; soilMat.uniforms.soilHealth.value = farmState.soil_health; const day = farmState.day || 1; let targetPhase = 0; if (day >= 80) targetPhase = 2; else if (day >= 40) targetPhase = 1; if (targetPhase !== currentCropPhase) { buildCropField(targetPhase); } clearWeatherEffects(); if (weatherCondition === 'rain' || weatherCondition === 'storm') { createRain(weatherCondition === 'storm' ? 2.0 : 1.0); } else if (weatherCondition === 'drought') { createDroughtHaze(); } setDayTime(day / 90); } const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const t = clock.getElapsedTime(); soilMat.uniforms.time.value = t; if (rainParticles) { const p = rainParticles.geometry.attributes.position.array; const v = rainParticles.geometry.attributes.velocity.array; for (let i = 0; i < p.length / 3; i++) { p[i*3+1] += v[i*3+1] * 0.016; p[i*3] += v[i*3] * 0.016; if (p[i*3+1] < 0) { p[i*3+1] = 25; p[i*3] = (Math.random()-0.5) * 60; } } rainParticles.geometry.attributes.position.needsUpdate = true; } for (let i = 0; i < cropMeshes.length; i++) { cropMeshes[i].rotation.z = Math.sin(t * 0.7 + i * 0.3) * 0.025; cropMeshes[i].rotation.x = Math.cos(t * 0.5 + i * 0.2) * 0.015; } controls.update(); renderer.render(scene, camera); } animate(); window.addEventListener('resize', () => { const w = window.innerWidth * 0.65; const h = window.innerHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); });