├── .gitignore ├── screenshot.png ├── package.json ├── LICENSE ├── Readme.md ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takahirox/aframe-rain/HEAD/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-rain", 3 | "version": "1.0.2", 4 | "description": "A-Frame rain component", 5 | "main": "index.js", 6 | "scripts": { 7 | "all": "npm run build && npm run build-uglify", 8 | "build": "webpack index.js build/aframe-rain.js", 9 | "build-uglify": "webpack -p index.js build/aframe-rain.min.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/takahirox/aframe-rain.git" 14 | }, 15 | "keywords": [ 16 | "aframe", 17 | "aframe-vr", 18 | "vr", 19 | "three", 20 | "threejs", 21 | "mozvr", 22 | "webvr" 23 | ], 24 | "author": "Takahiro (https://github.com/takahirox)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/takahirox/aframe-rain/issues" 28 | }, 29 | "homepage": "https://github.com/takahirox/aframe-rain#readme", 30 | "devDependencies": { 31 | "aframe": "^0.4.0", 32 | "three": "^0.83.0", 33 | "webpack": "^1.13.3" 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Takahiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # A-Frame Rain component 2 | 3 | aframe-rain is Rainfall effect component for A-Frame which displays a lot of 4 | rain drop/splash objects by using instancing technique with good performance. 5 | 6 | ![screenshot](./screenshot.png "screenshot") 7 | 8 | Closer rain drop height is shorter and further rain drop is more transparent, 9 | implemented similar to Fog effect technique. 10 | 11 | ## Demo 12 | 13 | [Demo](https://cdn.rawgit.com/takahirox/aframe-rain/v1.0.2/index.html) 14 | 15 | ## Properties 16 | 17 | ### aframe-rain 18 | 19 | | Properties | type | Default Value | Description | 20 | | ------------- | ------- | ------------- | ----------- | 21 | | color | color | '#ddf' | Rain drop/splash color | 22 | | count | int | 5000 | The number of drops/splashes | 23 | | depthDensity | number | 0.05 | Depth density which affects rain drop height/opacity. The name is from Fog density | 24 | | dropHeight | number | 1.0 | Rain drop height | 25 | | dropRadius | number | 0.005 | Rain drop radius | 26 | | height | number | 30.0 | How high rain drops fall from | 27 | | opacity | number | 0.4 | Rain drop/splash opacity | 28 | | splash | boolean | true | If displays rain splash on ground | 29 | | splashBounce | number | 4.0 | Rain splash bound strongness | 30 | | splashGravity | number | 9.8 * 4.0 | Rain splash gravity | 31 | | vector | vec3 | '0, -40.0 0' | Rain drop vector. y must be < 0.0 | 32 | | width | number | 30.0 | Area where rain drop/sphash effect | 33 | 34 | ## Browser 35 | 36 | ### How to use 37 | 38 | To apply Rain effect in a scene, add `rain` attribute in `` like ``. 39 | 40 | ```html 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | ## NPM 62 | 63 | ### How to install 64 | 65 | ``` 66 | $ npm install aframe-rain 67 | ``` 68 | 69 | ### How to build 70 | 71 | ``` 72 | $ npm install 73 | $ npm run all 74 | ``` 75 | 76 | ### How to load 77 | 78 | ``` 79 | require('aframe'); 80 | require('aframe-rain'); 81 | ``` -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Rain component 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 35 | 36 | 38 | 40 | 41 | 43 | 45 | 46 | 48 | 50 | 51 | 53 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Takahiro / https://github.com/takahirox 3 | */ 4 | 5 | if (typeof AFRAME === 'undefined') { 6 | throw new Error('Component attempted to register before' + 7 | 'AFRAME was available.'); 8 | } 9 | 10 | AFRAME.registerComponent('rain', { 11 | schema: { 12 | color: {type: 'color', default: '#ddf'}, 13 | count: {type: 'int', default: 5000}, 14 | depthDensity: {type: 'number', default: 0.05}, 15 | dropHeight: {type: 'number', default: 1.0}, 16 | dropRadius: {type: 'number', default: 0.005}, 17 | height: {type: 'number', default: 30.0}, 18 | opacity: {type: 'number', default: 0.4}, 19 | splash: {type: 'boolean', default: true}, 20 | splashBounce: {type: 'number', default: 4.0}, 21 | splashGravity: {type: 'number', default: 9.8 * 4.0}, 22 | vector: {type: 'vec3', default: '0, -40.0 0'}, 23 | width: {type: 'number', default: 30.0} 24 | }, 25 | 26 | init: function () { 27 | this.model = null; 28 | }, 29 | 30 | update: function () { 31 | this.remove(); 32 | this.initMeshes(); 33 | }, 34 | 35 | remove: function () { 36 | if (this.model === null) { return; } 37 | this.el.removeObject3D('mesh'); 38 | }, 39 | 40 | tick: function(time, delta) { 41 | if (this.model === null) { return; } 42 | this.model.material.uniforms.time.value += delta / 1000; 43 | }, 44 | 45 | initMeshes: function() { 46 | var el = this.el; 47 | var data = this.data; 48 | 49 | function createDropGeometry() { 50 | var geometry = new THREE.InstancedBufferGeometry(); 51 | 52 | var topMesh = new THREE.Mesh( 53 | new THREE.CylinderGeometry(0, data.dropRadius, 54 | data.dropHeight, undefined, undefined, true)); 55 | 56 | var bottomMesh = new THREE.Mesh( 57 | new THREE.SphereGeometry(data.dropRadius, undefined, 58 | undefined, undefined, undefined, Math.PI / 2, Math.PI / 2)); 59 | 60 | bottomMesh.position.y = -data.dropHeight * 0.5; 61 | 62 | var fallingVector = new THREE.Vector3(0, -1, 0); 63 | var vector = new THREE.Vector3( 64 | data.vector.x, data.vector.y, data.vector.z); 65 | 66 | topMesh.geometry.mergeMesh(bottomMesh); 67 | topMesh.geometry.lookAt(fallingVector.sub(vector.normalize())); 68 | 69 | geometry.copy( 70 | new THREE.BufferGeometry().fromGeometry(topMesh.geometry)); 71 | 72 | return geometry; 73 | } 74 | 75 | function getDropDuration() { 76 | return data.height / -data.vector.y; 77 | } 78 | 79 | function initDropGeometry(geometry) { 80 | var positionArray = geometry.attributes.position.array; 81 | var count = data.count; 82 | var vector = data.vector; 83 | var width = data.width; 84 | var height = data.height; 85 | 86 | var translateArray = new Float32Array(count * 3); 87 | var vectorArray = new Float32Array(count * 3); 88 | var timeOffsetArray = new Float32Array(count); 89 | var opacityArray = new Float32Array(positionArray.length / 3); 90 | 91 | var duration = getDropDuration(); 92 | for (var i = 0; i < count; i++) { 93 | vectorArray[i * 3 + 0] = vector.x + (Math.max(1.0, vector.x) * 94 | (Math.random() - 0.5) * 0.1); 95 | vectorArray[i * 3 + 1] = vector.y * (1.0 + Math.random() * 0.2); 96 | vectorArray[i * 3 + 2] = vector.z + (Math.max(1.0, vector.z) * 97 | (Math.random() - 0.5) * 0.1); 98 | 99 | translateArray[i * 3 + 0] = (Math.random() - 0.5) * width; 100 | translateArray[i * 3 + 1] = height; 101 | translateArray[i * 3 + 2] = (Math.random() - 0.5) * width; 102 | 103 | timeOffsetArray[i] = duration * Math.random(); 104 | } 105 | 106 | var minY = positionArray[1]; 107 | var maxY = positionArray[1]; 108 | for (var i = 1, il = positionArray.length / 3; i < il; i++) { 109 | var y = positionArray[i * 3 + 1]; 110 | minY = Math.min(y, minY); 111 | maxY = Math.max(y, maxY); 112 | } 113 | 114 | var baseOpacity = 0.0; 115 | for (var i = 0, il = positionArray.length / 3; i < il; i++) { 116 | var y = positionArray[i * 3 + 1]; 117 | opacityArray[i] = baseOpacity + 118 | (maxY - y) / (maxY - minY) * (1.0 - baseOpacity); 119 | } 120 | 121 | geometry.addAttribute('translate', 122 | new THREE.InstancedBufferAttribute(translateArray, 3, 1)); 123 | geometry.addAttribute('vector', 124 | new THREE.InstancedBufferAttribute(vectorArray, 3, 1)); 125 | geometry.addAttribute('timeOffset', 126 | new THREE.InstancedBufferAttribute(timeOffsetArray, 1, 1)); 127 | geometry.addAttribute('opacity2', 128 | new THREE.BufferAttribute(opacityArray, 1, 1)); 129 | } 130 | 131 | function createSplashGeometry() { 132 | var geometry = new THREE.InstancedBufferGeometry(); 133 | geometry.copy(new THREE.SphereBufferGeometry(data.dropRadius)); 134 | return geometry; 135 | } 136 | 137 | function getSplashDuration() { 138 | return data.splashBounce / (0.5 * data.splashGravity) * 2.3; 139 | } 140 | 141 | function initSplashGeometry(geometry) { 142 | var count = data.count; 143 | var width = data.width; 144 | var vector = data.vector; 145 | 146 | var vectorArray = new Float32Array(count*3); 147 | var translateArray = new Float32Array(count*3); 148 | var timeOffsetArray = new Float32Array(count); 149 | 150 | for (var i = 0; i < count; i++) { 151 | var angle = Math.random() * 2.0 * Math.PI; 152 | vectorArray[i*3+0] = Math.sin(angle); 153 | vectorArray[i*3+1] = data.splashBounce * (1.0 + Math.random() * 0.2); 154 | vectorArray[i*3+2] = Math.cos(angle); 155 | } 156 | 157 | var translateX, translateZ, timeOffset; 158 | var countPerGroup = 1; 159 | var dropDuration = getDropDuration(); 160 | var duration = getSplashDuration(); 161 | for (var i = 0; i < count; i++) { 162 | if (i % countPerGroup === 0) { 163 | translateX = dropDuration * vector.x + (Math.random() - 0.5) * width; 164 | translateZ = dropDuration * vector.z + (Math.random() - 0.5) * width; 165 | timeOffset = duration * Math.random(); 166 | } 167 | translateArray[i*3+0] = translateX; 168 | translateArray[i*3+1] = 0.0; 169 | translateArray[i*3+2] = translateZ; 170 | timeOffsetArray[i] = timeOffset; 171 | } 172 | 173 | geometry.addAttribute('vector', 174 | new THREE.InstancedBufferAttribute(vectorArray, 3, 1)); 175 | geometry.addAttribute('translate', 176 | new THREE.InstancedBufferAttribute(translateArray, 3, 1)); 177 | geometry.addAttribute('timeOffset', 178 | new THREE.InstancedBufferAttribute(timeOffsetArray, 1, 1)); 179 | } 180 | 181 | var dropGeometry = createDropGeometry(); 182 | var splashGeometry = createSplashGeometry(); 183 | 184 | initDropGeometry(dropGeometry); 185 | initSplashGeometry(splashGeometry); 186 | 187 | var dropUniforms = { 188 | time: {value: 0}, 189 | color: {value: new THREE.Color(data.color)}, 190 | opacity: {value: data.opacity} 191 | }; 192 | 193 | var dropVertexShader = [ 194 | '#define LOG2 1.442695', 195 | 'attribute float opacity2;', 196 | 'attribute vec3 translate;', 197 | 'attribute vec3 vector;', 198 | 'attribute float timeOffset;', 199 | 'uniform float time;', 200 | 'varying float vOpacity;', 201 | 'varying float vDepthFactor;', 202 | 'varying float vPositionY;', 203 | 'const float duration = float(' + getDropDuration() + ');', 204 | 'const float depthDensity = float(' + data.depthDensity + ');', 205 | 'const float depthDensity2 = depthDensity * depthDensity;', 206 | 'void main() {', 207 | ' float time2 = mod(time + timeOffset, duration);', 208 | ' vec3 offset = vector * time2;', 209 | ' vec3 pos = position + translate + offset;', 210 | ' vec3 pCenter = translate + offset;', 211 | ' vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);', 212 | ' float depth = mvPosition.z;', 213 | ' //float depth = length(mvPosition.xyz);', 214 | ' float depthFactor = clamp(exp2(-depthDensity2 * depth * depth * LOG2), 0.0, 1.0);', 215 | ' float sizeFactor = 1.0 - depthFactor * 0.5;', 216 | ' pos.y = pos.y * sizeFactor + pCenter.y * (1.0 - sizeFactor);', 217 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);', 218 | ' vOpacity = opacity2;', 219 | ' vDepthFactor = depthFactor;', 220 | ' vPositionY = pos.y;', 221 | '}' 222 | ].join('\n'); 223 | 224 | // TODO: light 225 | var dropFragmentShader = [ 226 | 'uniform vec3 color;', 227 | 'uniform float opacity;', 228 | 'varying float vOpacity;', 229 | 'varying float vDepthFactor;', 230 | 'varying float vPositionY;', 231 | 'void main() {', 232 | ' if (vPositionY < 0.0) { discard; }', 233 | ' float transparentFactor = 0.5 + vDepthFactor * 0.5;', 234 | ' gl_FragColor = vec4(color, opacity * vOpacity * transparentFactor);', 235 | '}' 236 | ].join('\n'); 237 | 238 | var splashUniforms = { 239 | time: dropUniforms.time, 240 | color: dropUniforms.color, 241 | opacity: {value: data.opacity * 0.8} 242 | }; 243 | 244 | var splashVertexShader = [ 245 | 'attribute vec3 vector;', 246 | 'attribute vec3 translate;', 247 | 'attribute float timeOffset;', 248 | 'uniform float time;', 249 | 'varying float vPositionY;', 250 | 'const float g = float(' + data.splashGravity + ');', 251 | 'const float duration = float(' + getSplashDuration() + ');', 252 | 'void main() {', 253 | ' float time2 = mod(time + timeOffset, duration);', 254 | ' vec3 offset;', 255 | ' offset.xz = vector.xz * time2;', 256 | ' offset.y = vector.y * time2 - 0.5 * g * time2 * time2;', 257 | ' vec3 pos = vec3(position.x * 2.0, position.y, position.z * 2.0) + offset + translate;', 258 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);', 259 | ' vPositionY = pos.y;', 260 | '}' 261 | ].join('\n'); 262 | 263 | // TODO: light 264 | var splashFragmentShader = [ 265 | 'uniform vec3 color;', 266 | 'uniform float opacity;', 267 | 'varying float vPositionY;', 268 | 'void main() {', 269 | ' if (vPositionY < 0.0) { discard; }', 270 | ' gl_FragColor = vec4(color, opacity);', 271 | '}' 272 | ].join('\n'); 273 | 274 | var dropMaterial = new THREE.ShaderMaterial({ 275 | uniforms: dropUniforms, 276 | vertexShader: dropVertexShader, 277 | fragmentShader: dropFragmentShader, 278 | transparent: true 279 | }); 280 | 281 | var splashMaterial = new THREE.ShaderMaterial({ 282 | uniforms: splashUniforms, 283 | vertexShader: splashVertexShader, 284 | fragmentShader: splashFragmentShader, 285 | transparent: true 286 | }); 287 | 288 | var dropMesh = new THREE.Mesh(dropGeometry, dropMaterial); 289 | var splashMesh = new THREE.Mesh(splashGeometry, splashMaterial); 290 | dropMesh.add(splashMesh); 291 | 292 | // TODO: set appropriate boundingSphere instead of setting frustumCulled false 293 | // for the optimization 294 | dropMesh.frustumCulled = false; 295 | splashMesh.frustumCulled = false; 296 | 297 | splashMesh.visible = data.splash; 298 | 299 | this.model = dropMesh; 300 | el.setObject3D('mesh', dropMesh); 301 | el.emit('model-loaded', {format:'mesh', model: dropMesh}); 302 | } 303 | }); 304 | --------------------------------------------------------------------------------