如何使用WebGL创建一个逼真的下雨动画

techbrood 发表于 2016-06-03 19:49:19

标签: webgl, rain, animation, 教程

- +

之前写过文章来分别讲解如何使用CSS3和Canvas2D实现过雨滴和下雨动画。

通过背景处理看起来也有视觉上的3D效果,但并非真正的3D场景,

如果要加入用户交互,进行360°全景浏览就很难实现,并且粒子放大后会失真。

今天我们使用WebGL来实现一个真正3D建模的下雨动画,所使用的技术可用于很多场景。

对WebGL没有概念的,请阅读踏得网之前写的WebGL基础知识相关文章。

要使用WebGL绘图,总体上有4个步骤,初始化webgl绘图环境、建立数据模型并绑定缓存、建立着色器程序、关联着色器属性和缓存完成绘制或动画。

要实现一个下雨动画,我们首先要实现一个3D雨滴的绘制。

3D雨滴具有如下特点:

  1. 雨滴是一个不规则椭球体,因为重力效应,下面胖上面瘦,Z轴长度比X/Y轴要长

  2. 雨滴是一个球透镜。球透镜是凸透镜的一种。凸透镜的成像原理:当物距(u)大于二倍焦距(f)时,所成像为倒立缩小的实像。


    下图右为平行但不经过主光轴的光线入射球透镜时的光路图;球透镜的焦距(f)即由球心(O)到焦点(F)的距离。

    [左]凸透镜的成像原理;[右]球透镜的光路图,入射光线平行但不经过主光轴。

    [左]凸透镜的成像原理;[右]球透镜的光路图,入射光线平行但不经过主光轴。

    经计算可以得出,球透镜的焦距为:

    /gkimage/e4/hp/2n/e4hp2n.png

    其中,N 为透镜材料的折射率,我们知道水的折射率约为1.33(玻璃的折射率为1.5~1.9),

    不难算出雨滴的焦距(约为) f 2R。

    在摄影/观察雨景过程中,物距一般远大于2倍焦距的,故透过雨滴观察到的,是远景的倒立缩小实像。

  3. 雨滴和水晶球一样,会产生“桶形畸变”,如下图

    桶形畸变(Barrel Distortion)又称桶形失真,是一种成像缺陷。使用广角镜头时最容易发生桶形畸变,原本是方形的物体影像,会变成四角向内收缩、边线中段则向外凸出,好象木桶一样。

  4. 由于存在重力加速度,雨滴将呈现加速下落的运动

  5. 雨滴的大小是随机的,而小雨滴容易受到风阻影响,所以原则上大雨滴的下落速度要快,也就是雨滴的速度是各自不同的。

我们完成一个雨滴对象的绘制后,在尺寸、形状和位置这个方面引入随机量,从而生成大量雨滴。

最后在Y方向上添加恒定加速度和随机阻力,形成下落的动画。

生成不规则椭球体

所谓创建一个球体,在WebGL中,我们是创建一个多面体,而多面体由一组顶点所定义。

我们模仿地球,使用经纬线来生成椭球体的各个顶点(vertex),坐标设定使用椭球体坐标公式:

x=asinθcosφ

y=bsinθsinφ

z=ccosθ (0≤θ≤π, 0≤φ<2π)

其中a,b,c是椭球体的3个半径,θ(theta)是z轴夹角,φ(phi)是投影到x/y平面上从x开始的夹角。

代码类似如下:(注意WebGL中的x/y/z坐标轴的方向和上述立体几何学中不完全相同)

var latitudeBands = 60;
var longitudeBands = 60;
var radius = 0.4;
var aRadius = radius * 1.2;
var bRadius = radius * 1.3;
var cRadius = radius * 1.0;
var vertexPositionData = [];
var normalData = [];
var textureCoordData = [];
for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) {
    var theta = latNumber * Math.PI / latitudeBands;
    var sinTheta = Math.sin(theta);
    var cosTheta = Math.cos(theta);
    
    for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) {
        var phi = longNumber * 2 * Math.PI / longitudeBands;
        var sinPhi = Math.sin(phi);
        var cosPhi = Math.cos(phi);
    
        var x = aRadius * cosPhi * sinTheta;
        var y = bRadius * cosTheta;
        var z = cRadius * sinPhi * sinTheta;
    
        vertexPositionData.push(radius * x);
        vertexPositionData.push(radius * y);
        vertexPositionData.push(radius * z);
    }
}

创建透镜效果

透镜效果最主要的就是要实现一个倒影图像,我们使用纹理贴图(Texture)来实现。

贴图到球面的投影,参考地图圆柱体投影方法(原理见下图)来实现:

Cylindrical Projection basics2.svg

不同的是,只需要投影正面区域,因为我们一般只观察正前方区域,我们可以制作一个半面的贴图。

我们可以直接把贴图旋转180°,或者通过投影时给y坐标一个负数值来实现倒影效果。

//纹理图片加载完成时执行的回调函数
function handleLoadedTexture(texture) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);//把几何坐标转换成屏幕坐标,即Y轴翻转
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);//使用TEXTURE0来绑定贴图
    //设置纹理缩放过滤器参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
    gl.generateMipmap(gl.TEXTURE_2D);

    gl.bindTexture(gl.TEXTURE_2D, null);
}

var dropTexture;
function initTexture() {
    dropTexture = gl.createTexture();
    dropTexture.image = new Image();
    dropTexture.image.onload = function() {
        handleLoadedTexture(dropTexture)
    }

    dropTexture.image.src = "//wow.techbrood.com/assets/love_half_r.png";
}

//按照外切圆柱投影法生成顶点纹理贴图的坐标
for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) {
    aRadius -= latNumber * radius / 1000;
    var theta = latNumber * Math.PI / latitudeBands;
    var sinTheta = Math.sin(theta);
    var cosTheta = Math.cos(theta);

    for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) {
        var u = 1 - (longNumber / longitudeBands);
        var v = 1 - (latNumber / latitudeBands);

        textureCoordData.push(u);
        textureCoordData.push(v);
    }
}

我们通过上面的方法处理球面贴图时,会同时生成相应的桶形变形效果。

随机雨滴的生成和动画

接下来我们给雨滴的形状、大小、位置以及速度添加随机分量,来模拟下雨的动画。

function drawScene() {
   //......
    for (var i = 0; i < SPHERE_NUM; i++) {
        g_mMatrix[i][13] -= 0.1 + i * Math.random() * 0.002;//随机速度
        if (g_mMatrix[i][13] < -6.5) {//随机位置
            g_mMatrix[i][12] = Math.random() * 30.0 - 15.0;
            g_mMatrix[i][13] = 6.5;
            g_mMatrix[i][14] = Math.random() * 4.0 - 20.0;
        };

        //使用纹理TEXTURE0
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, dropTexture);
        gl.uniform1i(shaderProgram.samplerUniform, 0);

        var blending = true;
        if (blending) {//使用混合模式
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
            gl.enable(gl.BLEND);
            gl.disable(gl.DEPTH_TEST);
            gl.uniform1f(shaderProgram.alphaUniform, 1.0);
        } else {
            gl.disable(gl.BLEND);
            gl.enable(gl.DEPTH_TEST);
        }

    }
    //......
}

function initBuffer() {
    //......
    var latitudeBands = 60;
    var longitudeBands = 60;
    var radius = 0.5;
    //创建随机形状和大小的椭球体,保持总体形状为一个蒜头形
    var aRadius = radius * (1.2 + Math.random());
    var bRadius = radius * (1.3 + Math.random());
    var cRadius = radius * 1.0;
    var vertexPositionData = [];
    var normalData = [];
    var textureCoordData = [];
    for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) {
        aRadius -= latNumber * radius / 1000;
        var theta = latNumber * Math.PI / latitudeBands;
        var sinTheta = Math.sin(theta);
        var cosTheta = Math.cos(theta);

        for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) {
            var phi = longNumber * 2 * Math.PI / longitudeBands;
            var sinPhi = Math.sin(phi);
            var cosPhi = Math.cos(phi);

            var x = aRadius * cosPhi * sinTheta;
            var y = -(bRadius * cosTheta);
            var z = cRadius * sinPhi * sinTheta;

            normalData.push(x);
            normalData.push(y);
            normalData.push(z);

            vertexPositionData.push(radius * x);
            vertexPositionData.push(radius * y);
            vertexPositionData.push(radius * z);
        }
    }
    //......
}

最后我们编写动画程序的主流程,动画我们使用本站这篇文章中介绍过的requestAnimationFrame接口:

function main() {
    window.scrollTo(0, 0);

    var canvas = document.getElementById("container");
    initGL(canvas);
    initShaders();
    initBuffers();
    initTexture();

    //gl.clearColor(0.0, 0.0, 0.0, 1.0); //clear background
    gl.enable(gl.DEPTH_TEST);

    requestAnimationFrame(drawScene());
}

window.addEventListener('load', main);


possitive(17) views21715 comments0

发送私信

最新评论

请先 登录 再评论.
相关文章
  • 2019年NodeJS框架Koa和Express选型比较

    Koa和Express都是NodeJS的主流应用开发框架。
    Express是一个完整的nodejs应用框架。Koa是由Express团队开发的,但是它有不同的关注点。Koa致力于核心中间件...

  • CSS3原生变量(Native Variables)新特性简介

    对Web开发者来说,一个盼望已久的特性是CSS终于支持原生变量了!
    变量是程序语言中用来解决代码重复和进行表达式计算的关键概念(想想数学方程式中的x)。...

  • ES6小知识:动态对象键(Dynamic Object Keys)语法简介

    在ES5,对象的键(key)总是被解释为字符串。ES6允许我们使用计算的值作为对象的键,使用方括号:[myKey]const

  • 常见面试题JavaScript闭包(ES5语法)

    JavaScript闭包(Closure)是常见的JS面试题,是否理解闭包是一个简单的区分JS初级和高级程序员的判例。几乎每个JS程序员都在使用闭包,有意或无意间。比如编写一个jQuery鼠标点击处理函数:$(function()

  • 使用CSS3 box-decoration-break特性实现多行文本样式

    当文章中的长文本被自动断行为多行文本时,其样式可能会出乎我们的设计。本文介绍如何使用CSS3中的box-decoration-break特性来处理多行元素样式。
    按照规范...

  • NodeJS、Java和PHP性能考量和若干参考结论

    首先需要说明的是,严格而言NodeJS和Java、PHP并非对等概念,NodeJS是基于JS的一个应用程序,而Java/PHP是语言。我们这里实际指的是分别使用node、java和php来实...

  • WebGL 纹理映射模式以及WRAP_S | WRAP_T参数详解

    我们在纹理滤镜一文中已经说明了2个重要的纹理参数,用来定义对象缩放时纹理的处理方式:GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER本文讲解其余几个纹理参数...

  • Three.js入门教程6 - 创建全景图和纹理

    全景图非常酷。使用Three.js做一个属于自己的全景图并不是那么困难。要做一个全景图,你需要一个软件用来做一张全景图片。我使用了iPhone上的Microsoft Photosyn...

  • Three.js入门教程5 - 10个必须知道的编程技巧

    作者为Google的Paul,关于如何写出好的WebGL代码的文章。和很多开发者一样,我通过实践学习,但同时我也向其他更有经验的开发者们学习。在过去的几个月中,我在c...

  • Three.js入门教程2 - 着色器(下)

    这是WebGL着色器教程的后半部分,如果你没看过前一篇,阅读这一篇教程可能会使你感到困惑,建议你翻阅前面的教程。

  • WebGL入门教程1 - 3D绘图基础知识

    现代浏览器努力使得Web用户体验更为丰富,而WebGL正处于这样的技术生态系统的中心位置。其应用范围覆盖在线游戏、大数据可视化、计算机辅助设计、虚拟现实以及数...

  • 使用top/left/margin和CSS3 translate两种方法实现标题居中的性能差异详解

    要实现标题全屏居中(同时在垂直和水平方向居中),有若干种方法,包括使用弹性布局、表格单元、绝对定位、自动外边距和CSS3平移变换等。你可能已经使用了这些方...

  • 如何使用纯CSS3实现一个3D泡沫

    要实现一个逼真的泡沫,涉及到比较复杂的光学/物理学知识。我们这里先简化下问题,实现一个相对简单而足够实用的泡沫元素。我们可以把基础的泡沫元素应用在很多场景中,比如水景、泡咖啡、啤酒甚至火焰特效中。泡沫首先是一个圆形元素.bubble

  • 在PHP网页程序中执行Sass/Compass命令

    我们需要在wow云开发平台支持sass/compass等预编译样式语言,为此我们首先尝试了scssphp扩展,但是在支持最新语法上,经常会出现异常。所以我们采用了代理的方式...

  • 更多...