Трава на канвасе
Реалистичная трава, колышущаяся от ветра. Камеру можно двигать и смотреть с разных ракурсов. Сделано на three.js
HTML
<canvas id="canvas"></canvas>
<div class="label">GRASS</div>
<div class="instructions">DRAG TO MOVE CAMERA</div>
CSS
body {
background-color: #fff;
margin: 0;
overflow: hidden;
}
.label {
position: absolute;
top: 0;
left: 0;
padding: 5px 15px;
color: #fff;
font-size: 13px;
background-color: rgba(0, 0, 0, .15);
}
.instructions {
position: absolute;
bottom: 0%;
left: 0;
padding: 5px 15px;
color: #fff;
font-size: 13px;
background-color: rgba(0, 0, 0, .15);
}
canvas { display:block; }
JS
Подключаемые библиотеки:https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.min.js
https://threejs.org/examples/js/controls/OrbitControls.js
https://s3-us-west-2.amazonaws.com/s.cdpn.io/896175/perlin.js
https://cdnjs.cloudflare.com/ajax/libs/stats.js/r16/Stats.min.js
скрипт//Based on:
//"Realistic real-time grass rendering" by Eddie Lee, 2010
//https://www.eddietree.com/grass
//https://medium.com/@Zadvorsky/into-vertex-shaders-594e6d8cd804u
//https://github.com/zadvorsky/three.bas
//https://github.com/mrdoob/three.js/blob/master/examples/webgl_buffergeometry_instancing_dynamic.html
//https://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/
var canvas = document.getElementById("canvas");
var TWO_PI = Math.PI*2;
const mobile = ( navigator.userAgent.match(/Android/i)
|| navigator.userAgent.match(/webOS/i)
|| navigator.userAgent.match(/iPhone/i)
|| navigator.userAgent.match(/BlackBerry/i)
|| navigator.userAgent.match(/Windows Phone/i)
);
//Variables for blade mesh
var joints = 5;
var w_ = 0.12;
var h_ = 1;
//Patch side length
var width = 120;
//Number of blades
var instances = 50000;
if(mobile){
instances = 10000;
width = 50;
}
//Camera rotate
var rotate = false;
//Initialise three.js
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer({antialias: true, canvas: canvas});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0x66deff, 1);
distance = 400;
var FOV = 2 * Math.atan( window.innerHeight / ( 2 * distance ) ) * 90 / Math.PI;
//Camera
var camera = new THREE.PerspectiveCamera(FOV, window.innerWidth / window.innerHeight, 1, 20000);
camera.position.set(-50, 10, 50);
scene.add(camera);
window.addEventListener( 'resize', onWindowResize, false );
function onWindowResize(){
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
//Lights
var light_1 = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(light_1);
//OrbitControls.js for camera manipulation
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.autoRotate = rotate;
controls.autoRotateSpeed = 0.5;
const stats = new Stats();
stats.showPanel(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.right = '0px';
stats.domElement.style.bottom = '0px';
document.body.appendChild(stats.domElement);
//http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
function multiplyQuaternions(q1, q2){
x = q1.x * q2.w + q1.y * q2.z - q1.z * q2.y + q1.w * q2.x;
y = -q1.x * q2.z + q1.y * q2.w + q1.z * q2.x + q1.w * q2.y;
z = q1.x * q2.y - q1.y * q2.x + q1.z * q2.w + q1.w * q2.z;
w = -q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + q1.w * q2.w;
return new THREE.Vector4(x, y, z, w);
}
var vertexSource = `
precision mediump float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec3 offset;
attribute vec2 uv;
attribute vec4 orientation;
attribute float halfRootAngleSin;
attribute float halfRootAngleCos;
attribute float stretch;
uniform float time;
varying vec2 vUv;
varying float frc;
/*** WEBGL-NOISE FROM https://github.com/stegu/webgl-noise ***/
// Description : Array and textureless GLSL 2D simplex noise function.
// Author : Ian McEwan, Ashima Arts.
// Maintainer : stegu
// Lastmod : 20110822 (ijm)
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
// https://github.com/stegu/webgl-noise
//
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v)
{
const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
-0.577350269189626, // -1.0 + 2.0 * C.x
0.024390243902439); // 1.0 / 41.0
// First corner
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
// Other corners
vec2 i1;
//i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
//i1.y = 1.0 - i1.x;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
// x0 = x0 - 0.0 + 0.0 * C.xx ;
// x1 = x0 - i1 + 1.0 * C.xx ;
// x2 = x0 - 1.0 + 2.0 * C.xx ;
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
// Permutations
i = mod289(i); // Avoid truncation effects in permutation
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
// Gradients: 41 points uniformly over a line, mapped onto a diamond.
// The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
// Normalise gradients implicitly by scaling m
// Approximation of: m *= inversesqrt( a0*a0 + h*h );
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
// Compute final noise value at P
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
//*** END NOISE ***
//https://www.geeks3d.com/20141201/how-to-rotate-a-vertex-by-a-quaternion-in-glsl/
vec3 rotateVectorByQuaternion( vec3 v, vec4 q){
return 2.0 * cross(q.xyz, v * q.w + cross(q.xyz, v)) + v;
}
//https://en.wikipedia.org/wiki/Slerp
vec4 slerp(vec4 v0, vec4 v1, float t) {
// Only unit quaternions are valid rotations.
// Normalize to avoid undefined behavior.
normalize(v0);
normalize(v1);
// Compute the cosine of the angle between the two vectors.
float dot_ = dot(v0, v1);
// If the dot product is negative, slerp won't take
// the shorter path. Note that v1 and -v1 are equivalent when
// the negation is applied to all four components. Fix by
// reversing one quaternion.
if (dot_ < 0.0) {
v1 = -v1;
dot_ = -dot_;
}
const float DOT_THRESHOLD = 0.9995;
if (dot_ > DOT_THRESHOLD) {
// If the inputs are too close for comfort, linearly interpolate
// and normalize the result.
vec4 result = t*(v1 - v0) + v0;
normalize(result);
return result;
}
// Since dot is in range [0, DOT_THRESHOLD], acos is safe
float theta_0 = acos(dot_); // theta_0 = angle between input vectors
float theta = theta_0*t; // theta = angle between v0 and result
float sin_theta = sin(theta); // compute this value only once
float sin_theta_0 = sin(theta_0); // compute this value only once
float s0 = cos(theta) - dot_ * sin_theta / sin_theta_0; // == sin(theta_0 - theta) / sin(theta_0)
float s1 = sin_theta / sin_theta_0;
return (s0 * v0) + (s1 * v1);
}
void main() {
//Relative position of vertex along the mesh Y direction
frc = position.y/float(` + h_ + `);
//Get wind data from simplex noise
float noise = 0.8-(snoise(vec2((time-offset.x/50.0), (time-offset.z/50.0))));
//Define the direction of an unbent blade of grass rotated around the Y axis
vec4 direction = vec4(0.0, halfRootAngleSin, 0.0, halfRootAngleCos);
//Interpolate between the unbent direction and the direction of growth calculated on the CPU.
//Using the relative location of the vertex along the Y axis as the weight, we get a smooth bend
direction = slerp(direction, orientation, frc);
vec3 vPosition = vec3(position.x, position.y + position.y * stretch, position.z);
vPosition = rotateVectorByQuaternion(vPosition, direction);
//Apply wind
float angle = noise * 0.3;
vPosition = rotateVectorByQuaternion(vPosition, normalize(vec4(sin(angle/2.0), 0.0, -sin(angle/2.0), cos(angle/1.0))));
//UV for texture
vUv = uv;
//Calculate final position of the vertex from the world offset and the above shenanigans
gl_Position = projectionMatrix * modelViewMatrix * vec4(offset + vPosition, 1.0 );
}`;
var fragmentSource = `
precision mediump float;
uniform sampler2D map;
uniform sampler2D alphaMap;
varying vec2 vUv;
varying float frc;
void main() {
//Get transparency information from alpha map
float alpha = texture2D(alphaMap, vUv).r;
//If transparent, don't draw
if(alpha < 0.15){
discard;
}
//Get colour data from texture
vec4 col = vec4(texture2D(map, vUv));
//Add more green towards root
col = mix(vec4(0.0, 0.6, 0.0, 1.0), col, frc);
//Add a shadow towards root
col = mix(vec4(0.0, 0.1, 0.0, 1.0), col, frc);
gl_FragColor = col;
}`;
//Use noise.js library to generate a grid of 2D simplex noise values
noise.seed(Math.random());
function getYPosition(x, z){
var y = 2*noise.simplex2(x/50, z/50);
y += 4*noise.simplex2(x/100, z/100);
y += 0.2*noise.simplex2(x/10, z/10);
return y;
};
//The ground
var ground_geometry = new THREE.PlaneGeometry(width, width, 32, 32);
ground_geometry.lookAt(new THREE.Vector3(0,1,0));
ground_geometry.verticesNeedUpdate = true;
var ground_material = new THREE.MeshPhongMaterial({color: 0x002300});
var ground = new THREE.Mesh(ground_geometry, ground_material);
for (var i = 0; i < ground.geometry.vertices.length; i++){
var v = ground.geometry.vertices[i];
v.y = getYPosition(v.x, v.z);
}
ground.geometry.computeVertexNormals();
scene.add(ground);
//Define base geometry that will be instanced. We use a plane for an individual blade of grass
var base_geometry = new THREE.PlaneBufferGeometry(w_, h_, 1, joints);
base_geometry.translate(0,h_/2,0);
var base_material = new THREE.MeshPhongMaterial({color: 0xff0000, side: THREE.DoubleSide});
var base_blade = new THREE.Mesh(base_geometry, base_material);
//From:
//https://github.com/mrdoob/three.js/blob/master/examples/webgl_buffergeometry_instancing_dynamic.html
var instanced_geometry = new THREE.InstancedBufferGeometry();
//----------ATTRIBUTES----------//
instanced_geometry.index = base_geometry.index;
instanced_geometry.attributes.position = base_geometry.attributes.position;
instanced_geometry.attributes.uv = base_geometry.attributes.uv;
// Each instance has its own data for position, rotation and scale
var offsets = [];
var orientations = [];
var stretches = [];
var halfRootAngleSin = [];
var halfRootAngleCos = [];
//Temp variables
var quaternion_0 = new THREE.Vector4();
var quaternion_1 = new THREE.Vector4();
var x, y, z, w;
//The min and max angle for the growth direction (in radians)
var min = -0.25;
var max = 0.25;
//For each instance of the grass blade
for (var i = 0; i < instances; i++){
//Offset of the roots
x = Math.random() * width - width/2;
z = Math.random() * width - width/2;
y = getYPosition(x, z);
offsets.push( x, y, z);
//Define random growth directions
//Rotate around Y
var angle = Math.PI - Math.random() * (2 * Math.PI);
halfRootAngleSin.push(Math.sin(0.5*angle));
halfRootAngleCos.push(Math.cos(0.5*angle));
var RotationAxis = new THREE.Vector3(0, 1, 0);
var x = RotationAxis.x * Math.sin(angle / 2.0);
var y = RotationAxis.y * Math.sin(angle / 2.0);
var z = RotationAxis.z * Math.sin(angle / 2.0);
var w = Math.cos(angle / 2.0);
quaternion_0.set( x, y, z, w).normalize();
//Rotate around X
angle = Math.random() * (max - min) + min;
RotationAxis = new THREE.Vector3(1, 0, 0);
x = RotationAxis.x * Math.sin(angle / 2.0);
y = RotationAxis.y * Math.sin(angle / 2.0);
z = RotationAxis.z * Math.sin(angle / 2.0);
w = Math.cos(angle / 2.0);
quaternion_1.set( x, y, z, w).normalize();
//Combine rotations to a single quaternion
quaternion_0 = multiplyQuaternions(quaternion_0, quaternion_1);
//Rotate around Z
angle = Math.random() * (max - min) + min;
RotationAxis = new THREE.Vector3(0, 0, 1);
x = RotationAxis.x * Math.sin(angle / 2.0);
y = RotationAxis.y * Math.sin(angle / 2.0);
z = RotationAxis.z * Math.sin(angle / 2.0);
w = Math.cos(angle / 2.0);
quaternion_1.set( x, y, z, w).normalize();
//Combine rotations to a single quaternion
quaternion_0 = multiplyQuaternions(quaternion_0, quaternion_1);
orientations.push(quaternion_0.x, quaternion_0.y, quaternion_0.z, quaternion_0.w);
//Define variety in height
if(i < instances/3){
stretches.push(Math.random() * 1.8);
}else{
stretches.push(Math.random());
}
}
var offsetAttribute = new THREE.InstancedBufferAttribute( new Float32Array( offsets ), 3);
var stretchAttribute = new THREE.InstancedBufferAttribute( new Float32Array( stretches ), 1);
var halfRootAngleSinAttribute = new THREE.InstancedBufferAttribute( new Float32Array( halfRootAngleSin ), 1);
var halfRootAngleCosAttribute = new THREE.InstancedBufferAttribute( new Float32Array( halfRootAngleCos ), 1);
var orientationAttribute = new THREE.InstancedBufferAttribute( new Float32Array( orientations ), 4);
instanced_geometry.addAttribute( 'offset', offsetAttribute);
instanced_geometry.addAttribute( 'orientation', orientationAttribute);
instanced_geometry.addAttribute( 'stretch', stretchAttribute);
instanced_geometry.addAttribute( 'halfRootAngleSin', halfRootAngleSinAttribute);
instanced_geometry.addAttribute( 'halfRootAngleCos', halfRootAngleCosAttribute);
//Get alpha map and blade texture
//These have been taken from "Realistic real-time grass rendering" by Eddie Lee, 2010
THREE.ImageUtils.crossOrigin = '';
var texture = THREE.ImageUtils.loadTexture("https://res.cloudinary.com/al-ro/image/upload/v1552838655/v2_iqvzcx.png");
var alphaMap = THREE.ImageUtils.loadTexture("https://res.cloudinary.com/al-ro/image/upload/v1552834315/Screen_Shot_2019-03-17_at_15.50.35_y5zfyu.png");
//Define the material, specifying attributes, uniforms, shaders etc.
var material = new THREE.RawShaderMaterial( {
uniforms: {
map: { value: texture},
alphaMap: { value: alphaMap},
time: {type: 'float', value: 0}
},
vertexShader: vertexSource,
fragmentShader: fragmentSource,
side: THREE.DoubleSide
} );
mesh = new THREE.Mesh( instanced_geometry, material );
scene.add(mesh);
//Show base geometry
//scene.add(base_blade);
//----------DRAW----------//
var time = 0;
function draw(){
stats.begin();
time += 1/100;
material.uniforms.time.value = time;
renderer.render(scene, camera);
if(rotate){
controls.update();
}
stats.end();
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);