| |
| |
| |
| import * as THREE from 'three'; |
|
|
| export function createSky(scene) { |
| const skyGeo = new THREE.SphereGeometry(400, 32, 32); |
| const skyMat = new THREE.ShaderMaterial({ |
| side: THREE.BackSide, |
| depthWrite: false, |
| uniforms: { |
| uTop: { value: new THREE.Color(0x1a1a3e) }, |
| uMid: { value: new THREE.Color(0xb07088) }, |
| uBot: { value: new THREE.Color(0xffa860) }, |
| uSun: { value: new THREE.Color(0xffee88) }, |
| uSunDir: { value: new THREE.Vector3(0.6, 0.01, 0.3).normalize() }, |
| }, |
| vertexShader: ` |
| varying vec3 vWorldPos; |
| void main() { |
| vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| } |
| `, |
| fragmentShader: ` |
| uniform vec3 uTop, uMid, uBot, uSun; |
| uniform vec3 uSunDir; |
| varying vec3 vWorldPos; |
| void main() { |
| vec3 dir = normalize(vWorldPos); |
| float y = dir.y; |
| vec3 col = mix(uMid, uTop, smoothstep(0.1, 0.8, y)); |
| col = mix(uBot, col, smoothstep(-0.02, 0.15, y)); |
| float sunDot = max(dot(dir, uSunDir), 0.0); |
| col += uSun * pow(sunDot, 64.0) * 2.0; |
| col += uSun * pow(sunDot, 6.0) * 0.4; |
| col += vec3(1.0, 0.8, 0.5) * pow(sunDot, 3.0) * 0.15; |
| |
| gl_FragColor = vec4(col, 1.0); |
| } |
| `, |
| }); |
| scene.add(new THREE.Mesh(skyGeo, skyMat)); |
| } |
|
|
| export function createLights(scene) { |
| scene.add(new THREE.AmbientLight(0x999088, 0.45)); |
| scene.add(new THREE.HemisphereLight(0xffccaa, 0x443322, 0.3)); |
|
|
| const sun = new THREE.DirectionalLight(0xffe8cc, 1.4); |
| sun.position.set(120, 6, 50); |
| sun.castShadow = true; |
| sun.shadow.mapSize.set(2048, 2048); |
| sun.shadow.bias = -0.0005; |
| const sc = sun.shadow.camera; |
| sc.left = sc.bottom = -130; |
| sc.right = sc.top = 130; |
| sc.near = 1; sc.far = 300; |
| scene.add(sun); |
| scene.add(sun.target); |
|
|
| const fill = new THREE.DirectionalLight(0x8877aa, 0.2); |
| fill.position.set(-40, 30, -60); |
| scene.add(fill); |
|
|
| return { sun }; |
| } |
|
|
| export function createGround(scene, renderer) { |
| const texSize = 512; |
| const texCanvas = document.createElement('canvas'); |
| texCanvas.width = texCanvas.height = texSize; |
| const ctx = texCanvas.getContext('2d'); |
| ctx.fillStyle = '#4a7a3a'; |
| ctx.fillRect(0, 0, texSize, texSize); |
| const imgData = ctx.getImageData(0, 0, texSize, texSize); |
| const d = imgData.data; |
| for (let i = 0; i < d.length; i += 4) { |
| const n = (Math.random() - 0.5) * 30; |
| d[i] = Math.max(0, Math.min(255, d[i] + n * 0.6)); |
| d[i+1] = Math.max(0, Math.min(255, d[i+1] + n)); |
| d[i+2] = Math.max(0, Math.min(255, d[i+2] + n * 0.4)); |
| } |
| for (let i = 0; i < 600; i++) { |
| const x = Math.random() * texSize, y = Math.random() * texSize; |
| const r = 2 + Math.random() * 4; |
| ctx.fillStyle = Math.random() > 0.7 ? '#3d6a2a' : '#5a8f40'; |
| ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); |
| } |
| for (let i = 0; i < 60; i++) { |
| const x = Math.random() * texSize, y = Math.random() * texSize; |
| ctx.fillStyle = ['#ff8866', '#ffcc44', '#ffaa77', '#cc7744'][Math.floor(Math.random() * 4)]; |
| ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI * 2); ctx.fill(); |
| } |
| ctx.putImageData(imgData, 0, 0); |
|
|
| const grassTex = new THREE.CanvasTexture(texCanvas); |
| grassTex.wrapS = grassTex.wrapT = THREE.RepeatWrapping; |
| grassTex.repeat.set(40, 40); |
| grassTex.anisotropy = renderer.capabilities.getMaxAnisotropy(); |
|
|
| const ground = new THREE.Mesh( |
| new THREE.PlaneGeometry(800, 800), |
| new THREE.MeshLambertMaterial({ map: grassTex }) |
| ); |
| ground.rotation.x = -Math.PI / 2; |
| ground.position.y = -0.1; |
| ground.receiveShadow = true; |
| scene.add(ground); |
| } |
|
|