setupScene() {
this.gl.enable(this.gl.DEPTH_TEST);
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
this.time = 0;
this.animals = [];
this.waterOffset = 0;
// Create some animals
for (let i = 0; i < 8; i++) {
this.animals.push({
x: Math.random() * 800 - 400,
z: Math.random() * 600 - 300,
speed: 0.5 + Math.random() * 1.5,
direction: Math.random() * Math.PI * 2,
type: Math.floor(Math.random() * 3), // 0: cow, 1: sheep, 2: chicken
size: 0.8 + Math.random() * 0.4
});
}
}
createShaders() {
const vertexShaderSource = `#version 300 es
in vec3 position;
in vec3 color;
uniform mat4 uProjection;
uniform mat4 uView;
uniform float uTime;
out vec3 vColor;
out vec3 vPosition;
void main() {
vColor = color;
vPosition = position;
vec3 pos = position;
// Add some gentle water movement
if (pos.y < -0.1) {
pos.y += sin(pos.x * 0.5 + uTime * 2.0) * 0.02;
pos.y += cos(pos.z * 0.3 + uTime * 1.5) * 0.015;
}
gl_Position = uProjection * uView * vec4(pos, 1.0);
}
`;
const fragmentShaderSource = `#version 300 es
precision mediump float;
in vec3 vColor;
in vec3 vPosition;
uniform float uTime;
out vec4 fragColor;
void main() {
vec3 color = vColor;
// Add some water shimmer
if (vPosition.y < -0.05) {
float shimmer = sin(vPosition.x * 10.0 + uTime * 5.0) * 0.1;
shimmer += cos(vPosition.z * 8.0 + uTime * 3.0) * 0.05;
color += vec3(shimmer * 0.3, shimmer * 0.5, shimmer * 0.7);
}
// Add atmospheric perspective
float distance = length(vPosition);
float fog = exp(-distance * 0.01);
color = mix(vec3(0.7, 0.8, 0.9), color, fog);
fragColor = vec4(color, 1.0);
}
`;
this.program = this.createProgram(vertexShaderSource, fragmentShaderSource);
}
createProgram(vertexSource, fragmentSource) {
const vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.gl.shaderSource(vertexShader, vertexSource);
this.gl.compileShader(vertexShader);
const fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
this.gl.shaderSource(fragmentShader, fragmentSource);
this.gl.compileShader(fragmentShader);
const program = this.gl.createProgram();
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
return program;
}
createGeometry() {
// Create terrain, river, trees, and Angus
const vertices = [];
const colors = [];
// Ground plane (grass)
for (let x = -500; x <= 500; x += 50) {
for (let z = -300; z <= 300; z += 50) {
const grassGreen = [0.2 + Math.random() * 0.3, 0.6 + Math.random() * 0.2, 0.1];
// Quad vertices
vertices.push(x, 0, z, x + 50, 0, z, x, 0, z + 50);
vertices.push(x + 50, 0, z, x + 50, 0, z + 50, x, 0, z + 50);
// Colors
for (let i = 0; i < 6; i++) {
colors.push(...grassGreen);
}
}
}
// River (blue strip)
for (let z = -300; z <= 300; z += 20) {
const riverBlue = [0.2, 0.4 + Math.random() * 0.2, 0.8];
vertices.push(-100, -0.1, z, 100, -0.1, z, -100, -0.1, z + 20);
vertices.push(100, -0.1, z, 100, -0.1, z + 20, -100, -0.1, z + 20);
for (let i = 0; i < 6; i++) {
colors.push(...riverBlue);
}
}
// Trees (simple green pyramids)
const treePositions = [
[-200, 50], [200, 80], [-300, -100], [250, -50],
[-150, 150], [180, 120], [-250, -200], [300, -150]
];
treePositions.forEach(([x, z]) => {
// Tree trunk (brown)
const trunkBrown = [0.4, 0.2, 0.1];
vertices.push(x - 5, 0, z - 5, x + 5, 0, z - 5, x - 5, 20, z - 5);
vertices.push(x + 5, 0, z - 5, x + 5, 20, z - 5, x - 5, 20, z - 5);
for (let i = 0; i < 6; i++) {
colors.push(...trunkBrown);
}
// Tree leaves (green pyramid)
const leafGreen = [0.1, 0.5, 0.1];
vertices.push(x - 20, 20, z - 20, x + 20, 20, z - 20, x, 50, z);
vertices.push(x + 20, 20, z - 20, x + 20, 20, z + 20, x, 50, z);
vertices.push(x + 20, 20, z + 20, x - 20, 20, z + 20, x, 50, z);
vertices.push(x - 20, 20, z + 20, x - 20, 20, z - 20, x, 50, z);
for (let i = 0; i < 12; i++) {
colors.push(...leafGreen);
}
});
// Angus (red-haired fisherman by the river)
const angusX = 50, angusZ = -250;
// Angus body (simple shapes)
const skinColor = [0.9, 0.7, 0.6];
const redHair = [0.8, 0.3, 0.1]; // Red hair!
const clothingBlue = [0.2, 0.3, 0.6];
// Head (with red hair)
vertices.push(angusX - 8, 35, angusZ - 8, angusX + 8, 35, angusZ - 8, angusX - 8, 50, angusZ - 8);
vertices.push(angusX + 8, 35, angusZ - 8, angusX + 8, 50, angusZ - 8, angusX - 8, 50, angusZ - 8);
for (let i = 0; i < 6; i++) colors.push(...redHair);
// Face
vertices.push(angusX - 6, 35, angusZ - 6, angusX + 6, 35, angusZ - 6, angusX - 6, 45, angusZ - 6);
vertices.push(angusX + 6, 35, angusZ - 6, angusX + 6, 45, angusZ - 6, angusX - 6, 45, angusZ - 6);
for (let i = 0; i < 6; i++) colors.push(...skinColor);
// Body
vertices.push(angusX - 10, 5, angusZ - 5, angusX + 10, 5, angusZ - 5, angusX - 10, 35, angusZ - 5);
vertices.push(angusX + 10, 5, angusZ - 5, angusX + 10, 35, angusZ - 5, angusX - 10, 35, angusZ - 5);
for (let i = 0; i < 6; i++) colors.push(...clothingBlue);
// Fishing rod
const rodBrown = [0.4, 0.2, 0.1];
vertices.push(angusX + 15, 25, angusZ, angusX + 40, 30, angusZ - 50, angusX + 15, 27, angusZ);
for (let i = 0; i < 3; i++) colors.push(...rodBrown);
this.vertexBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(vertices), this.gl.STATIC_DRAW);
this.colorBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.colorBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(colors), this.gl.STATIC_DRAW);
this.vertexCount = vertices.length / 3;
}
setupAnimationLoop() {
const animate = () => {
this.time += 0.016;
this.render();
requestAnimationFrame(animate);
};
animate();
}
render() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
this.gl.clearColor(0.7, 0.8, 0.9, 1.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
this.gl.useProgram(this.program);
// Set up camera
const aspect = this.canvas.width / this.canvas.height;
const projection = this.perspective(45 * Math.PI / 180, aspect, 0.1, 1000);
const cameraX = Math.sin(this.time * 0.2) * 300;
const cameraZ = Math.cos(this.time * 0.2) * 300;
const view = this.lookAt([cameraX, 100, cameraZ], [0, 0, 0], [0, 1, 0]);
const projectionLoc = this.gl.getUniformLocation(this.program, 'uProjection');
const viewLoc = this.gl.getUniformLocation(this.program, 'uView');
const timeLoc = this.gl.getUniformLocation(this.program, 'uTime');
this.gl.uniformMatrix4fv(projectionLoc, false, projection);
this.gl.uniformMatrix4fv(viewLoc, false, view);
this.gl.uniform1f(timeLoc, this.time);
// Bind vertex data
const positionLoc = this.gl.getAttribLocation(this.program, 'position');
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
this.gl.enableVertexAttribArray(positionLoc);
this.gl.vertexAttribPointer(positionLoc, 3, this.gl.FLOAT, false, 0, 0);
const colorLoc = this.gl.getAttribLocation(this.program, 'color');
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.colorBuffer);
this.gl.enableVertexAttribArray(colorLoc);
this.gl.vertexAttribPointer(colorLoc, 3, this.gl.FLOAT, false, 0, 0);
this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount);
}
perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
return new Float32Array([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) / (near - far), (2 * far * near) / (near - far),
0, 0, -1, 0
]);
}
lookAt(eye, center, up) {
const f = this.normalize(this.subtract(center, eye));
const s = this.normalize(this.cross(f, up));
const u = this.cross(s, f);
return new Float32Array([
s[0], u[0], -f[0], 0,
s[1], u[1], -f[1], 0,
s[2], u[2], -f[2], 0,
-this.dot(s, eye), -this.dot(u, eye), this.dot(f, eye), 1
]);
}
normalize(v) {
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / len, v[1] / len, v[2] / len];
}
subtract(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
}
// Initialize the farm scene
window.addEventListener('load', () => {
const canvas = document.getElementById('farmCanvas');
new FarmScene(canvas);
});
// Handle window resize
window.addEventListener('resize', () => {
const canvas = document.getElementById('farmCanvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});