目录

  • 1. 概述
  • 2. 原理
    • 2.1. 光源类型
    • 2.2. 反射类型
      • 2.2.1. 环境反射(enviroment/ambient reflection)
      • 2.2.2. 漫反射(diffuse reflection)
      • 2.2.3. 综合
  • 3. 实例
    • 3.1. 具体代码
    • 3.2. 改动详解
      • 3.2.1. 设置日照
      • 3.2.2. 着色器光照设置
  • 4. 结果
  • 5. 参考

1. 概述

红尘小说 www.zuxs.net

在上一篇教程《WebGL简易教程(九):综合实例:地形的绘制》中,实现了对一个地形场景的渲染。在这篇教程中,就给这个地形场景加上光照,让其更加真实,立体感更强。

2. 原理

2.1. 光源类型

在现实中,即使是一个纯白色的物体,你也能很容易识别物体的轮廓。事实上,这是因为光照的产生的阴暗差异给了其立体感。类似于现实,WebGL有三种基本类型的光:

  1. 点光源光:一个点向周围发出的光,如灯泡、火焰等。定义一个点光源光需要光源的位置、光线方向以及颜色。根据照射点的位置不同,光线的方向也不同。
  2. 平行光:平行光可以看成是无限远处的光源发出的光,如太阳光。因为离光源的位置特别远,所以到达被照物体时可以认为光线是平行的。只需要用一个方向和颜色来定义即可。
  3. 环境光:环境光也就是间接光,指的是那些光源发出后,经过其他物体各种发射,然后照到物体表面上的光线。比如说夜间打开冰箱的门,这个厨房产生的亮光。因为经过多次反射后,强度差距已经非常小,没有必要精确计算光线强度。所以一般认为环境光是均匀照射到物体表面的,只需要一个颜色来定义。

如图所示:

2.2. 反射类型

由于物体最终显示的颜色也就是光线反射造成的颜色,由两部分因素决定:入射光和物体表面的类型。入射光信息包括入射光的方向和颜色,而物体表面的信息包含基底色和反射特性。根据物体反射光线的方式有环境反射(enviroment/ambient reflection)和漫反射(diffuse reflection)两种类型的光:

2.2.1. 环境反射(enviroment/ambient reflection)

环境反射是针对环境光而言的,在环境反射中,环境光照射物体是各方面均匀、强度相等的,反射的方向可以认为就是入射光的反方向。也就是最终物体的颜色只跟入射光颜色和基底色有关。那么可以这样定义环境反射光颜色:
\[ <环境反射光颜色>=<入射光颜色>×<表面基底色>\tag{1} \]
注意在式子中,这个乘法操作指的是颜色矢量上逐分量相乘。

2.2.2. 漫反射(diffuse reflection)

漫反射是针对平行光和点光源光而言的。相信在初中物理的时候就已经接触过镜面反射和漫反射。如果物体表面像镜子一样平滑,那么光线就会以特定的角度反射过去,从视觉效果来说就是刺眼的反光效果;如果物体表面是凹凸不平的,反射光就会以不固定的角度发射出去。在现实中大多数的物体表面都是粗糙的,所以才能看清各种各样的物体。如图所示:

漫反射中,反射光的颜色除了取决于入射光的颜色、表面的基底色,还有入射光与物体表面的法向量形成的入射角。令入射角为θ,漫反射光的颜色可以根据下式计算:
\[ <漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ\tag{2} \]
入射角θ可以通过矢量的点积来计算:
\[ <光线方向>·<法线方向> = |光线方向|*|法线方向|*cosθ \]
如果光线方向和法线方向都是归一化的,那么向量的模(长度)就为1,则有:
\[ <漫反射光颜色>=<入射光颜色>×<表面基底色>×(<光线方向>·<法线方向>) \]
注意,这里的“光线方向”,实际上指的是入射方向的反方向,即从入射点指向光源方向,如图所示:

2.2.3. 综合

当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色:
\[ <表面的反射光颜色> = <漫反射光颜色>+<环境反射光颜色>\tag{3} \]

3. 实例

3.1. 具体代码

改进上一篇教程的JS代码如下:

// 顶点着色器程序
var VSHADER_SOURCE ='attribute vec4 a_Position;\n' + //位置'attribute vec4 a_Color;\n' + //颜色'attribute vec4 a_Normal;\n' + //法向量'uniform mat4 u_MvpMatrix;\n' +'varying vec4 v_Color;\n' +'varying vec4 v_Normal;\n' +'void main() {\n' +'  gl_Position = u_MvpMatrix * a_Position;\n' + //设置顶点的坐标'  v_Color = a_Color;\n' +'  v_Normal = a_Normal;\n' +'}\n';// 片元着色器程序
var FSHADER_SOURCE ='precision mediump float;\n' +'uniform vec3 u_DiffuseLight;\n' + // 漫反射光颜色'uniform vec3 u_LightDirection;\n' + // 漫反射光的方向'uniform vec3 u_AmbientLight;\n' + // 环境光颜色'varying vec4 v_Color;\n' +'varying vec4 v_Normal;\n' +'void main() {\n' +//对法向量归一化'  vec3 normal = normalize(v_Normal.xyz);\n' +//计算光线向量与法向量的点积'  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +//计算漫发射光的颜色 '  vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;\n' +//计算环境光的颜色'  vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +'  gl_FragColor = vec4(diffuse+ambient, v_Color.a);\n' +'}\n';//定义一个矩形体:混合构造函数原型模式
function Cuboid(minX, maxX, minY, maxY, minZ, maxZ) {this.minX = minX;this.maxX = maxX;this.minY = minY;this.maxY = maxY;this.minZ = minZ;this.maxZ = maxZ;
}Cuboid.prototype = {constructor: Cuboid,CenterX: function () {return (this.minX + this.maxX) / 2.0;},CenterY: function () {return (this.minY + this.maxY) / 2.0;},CenterZ: function () {return (this.minZ + this.maxZ) / 2.0;},LengthX: function () {return (this.maxX - this.minX);},LengthY: function () {return (this.maxY - this.minY);}
}//定义DEM
function Terrain() {}
Terrain.prototype = {constructor: Terrain,setWH: function (col, row) {this.col = col;this.row = row;}
}var currentAngle = [0.0, 0.0]; // 绕X轴Y轴的旋转角度 ([x-axis, y-axis])
var curScale = 1.0; //当前的缩放比例function main() {var demFile = document.getElementById('demFile');if (!demFile) {console.log("Failed to get demFile element!");return;}demFile.addEventListener("change", function (event) {//判断浏览器是否支持FileReader接口if (typeof FileReader == 'undefined') {console.log("你的浏览器不支持FileReader接口!");return;}var input = event.target;var reader = new FileReader();reader.onload = function () {if (reader.result) {//读取var terrain = new Terrain();if (!readDEMFile(reader.result, terrain)) {console.log("文件格式有误,不能读取该文件!");}//绘制onDraw(gl, canvas, terrain);}}reader.readAsText(input.files[0]);});// 获取 <canvas> 元素var canvas = document.getElementById('webgl');// 获取WebGL渲染上下文var gl = getWebGLContext(canvas);if (!gl) {console.log('Failed to get the rendering context for WebGL');return;}// 初始化着色器if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {console.log('Failed to intialize shaders.');return;}// 指定清空<canvas>的颜色gl.clearColor(0.0, 0.0, 0.0, 1.0);// 开启深度测试gl.enable(gl.DEPTH_TEST);//清空颜色和深度缓冲区gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}//绘制函数
function onDraw(gl, canvas, terrain) {// 设置顶点位置var n = initVertexBuffers(gl, terrain);if (n < 0) {console.log('Failed to set the positions of the vertices');return;}//注册鼠标事件initEventHandlers(canvas);//设置灯光setLight(gl);//绘制函数var tick = function () {//设置MVP矩阵setMVPMatrix(gl, canvas, terrain.cuboid);//清空颜色和深度缓冲区gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);//绘制矩形体gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0);//请求浏览器调用tickrequestAnimationFrame(tick);};//开始绘制tick();
}//设置灯光
function setLight(gl) {var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');var u_DiffuseLight = gl.getUniformLocation(gl.program, 'u_DiffuseLight');var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');if (!u_DiffuseLight || !u_LightDirection || !u_AmbientLight) {console.log('Failed to get the storage location');return;}//设置漫反射光gl.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0);// 设置光线方向(世界坐标系下的)var solarAltitude = 45.0;var solarAzimuth = 315.0;var fAltitude = solarAltitude * Math.PI / 180; //光源高度角var fAzimuth = solarAzimuth * Math.PI / 180; //光源方位角var arrayvectorX = Math.cos(fAltitude) * Math.cos(fAzimuth);var arrayvectorY = Math.cos(fAltitude) * Math.sin(fAzimuth);var arrayvectorZ = Math.sin(fAltitude);var lightDirection = new Vector3([arrayvectorX, arrayvectorY, arrayvectorZ]);lightDirection.normalize(); // Normalizegl.uniform3fv(u_LightDirection, lightDirection.elements);//设置环境光gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
}//读取DEM函数
function readDEMFile(result, terrain) {var stringlines = result.split("\n");if (!stringlines || stringlines.length <= 0) {return false;}//读取头信息var subline = stringlines[0].split("\t");if (subline.length != 6) {return false;}var col = parseInt(subline[4]); //DEM宽var row = parseInt(subline[5]); //DEM高var verticeNum = col * row;if (verticeNum + 1 > stringlines.length) {return false;}terrain.setWH(col, row);//读取点信息var ci = 0;var pSize = 9;terrain.verticesColors = new Float32Array(verticeNum * pSize);for (var i = 1; i < stringlines.length; i++) {if (!stringlines[i]) {continue;}var subline = stringlines[i].split(',');if (subline.length != pSize) {continue;}for (var j = 0; j < pSize; j++) {terrain.verticesColors[ci] = parseFloat(subline[j]);ci++;}}if (ci !== verticeNum * pSize) {return false;}//包围盒var minX = terrain.verticesColors[0];var maxX = terrain.verticesColors[0];var minY = terrain.verticesColors[1];var maxY = terrain.verticesColors[1];var minZ = terrain.verticesColors[2];var maxZ = terrain.verticesColors[2];for (var i = 0; i < verticeNum; i++) {minX = Math.min(minX, terrain.verticesColors[i * pSize]);maxX = Math.max(maxX, terrain.verticesColors[i * pSize]);minY = Math.min(minY, terrain.verticesColors[i * pSize + 1]);maxY = Math.max(maxY, terrain.verticesColors[i * pSize + 1]);minZ = Math.min(minZ, terrain.verticesColors[i * pSize + 2]);maxZ = Math.max(maxZ, terrain.verticesColors[i * pSize + 2]);}terrain.cuboid = new Cuboid(minX, maxX, minY, maxY, minZ, maxZ);return true;
}//注册鼠标事件
function initEventHandlers(canvas) {var dragging = false; // Dragging or notvar lastX = -1,lastY = -1; // Last position of the mouse//鼠标按下canvas.onmousedown = function (ev) {var x = ev.clientX;var y = ev.clientY;// Start dragging if a moue is in <canvas>var rect = ev.target.getBoundingClientRect();if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {lastX = x;lastY = y;dragging = true;}};//鼠标离开时canvas.onmouseleave = function (ev) {dragging = false;};//鼠标释放canvas.onmouseup = function (ev) {dragging = false;};//鼠标移动canvas.onmousemove = function (ev) {var x = ev.clientX;var y = ev.clientY;if (dragging) {var factor = 100 / canvas.height; // The rotation ratiovar dx = factor * (x - lastX);var dy = factor * (y - lastY);currentAngle[0] = currentAngle[0] + dy;currentAngle[1] = currentAngle[1] + dx;}lastX = x, lastY = y;};//鼠标缩放canvas.onmousewheel = function (event) {if (event.wheelDelta > 0) {curScale = curScale * 1.1;} else {curScale = curScale * 0.9;}};
}//设置MVP矩阵
function setMVPMatrix(gl, canvas, cuboid) {// Get the storage location of u_MvpMatrixvar u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');if (!u_MvpMatrix) {console.log('Failed to get the storage location of u_MvpMatrix');return;}//模型矩阵var modelMatrix = new Matrix4();modelMatrix.scale(curScale, curScale, curScale);modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis modelMatrix.translate(-cuboid.CenterX(), -cuboid.CenterY(), -cuboid.CenterZ());//投影矩阵var fovy = 60;var near = 1;var projMatrix = new Matrix4();projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1, 10000);//计算lookAt()函数初始视点的高度var angle = fovy / 2 * Math.PI / 180.0;var eyeHight = (cuboid.LengthY() * 1.2) / 2.0 / angle;//视图矩阵  var viewMatrix = new Matrix4(); // View matrix   viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);//MVP矩阵var mvpMatrix = new Matrix4();mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);//将MVP矩阵传输到着色器的uniform变量u_MvpMatrixgl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
}//
function initVertexBuffers(gl, terrain) {//DEM的一个网格是由两个三角形组成的//      0------1            1//      |                   |//      |                   |//      col       col------col+1    var col = terrain.col;var row = terrain.row;var indices = new Uint16Array((row - 1) * (col - 1) * 6);var ci = 0;for (var yi = 0; yi < row - 1; yi++) {//for (var yi = 0; yi < 10; yi++) {for (var xi = 0; xi < col - 1; xi++) {indices[ci * 6] = yi * col + xi;indices[ci * 6 + 1] = (yi + 1) * col + xi;indices[ci * 6 + 2] = yi * col + xi + 1;indices[ci * 6 + 3] = (yi + 1) * col + xi;indices[ci * 6 + 4] = (yi + 1) * col + xi + 1;indices[ci * 6 + 5] = yi * col + xi + 1;ci++;}}//var verticesColors = terrain.verticesColors;var FSIZE = verticesColors.BYTES_PER_ELEMENT; //数组中每个元素的字节数// 创建缓冲区对象var vertexColorBuffer = gl.createBuffer();var indexBuffer = gl.createBuffer();if (!vertexColorBuffer || !indexBuffer) {console.log('Failed to create the buffer object');return -1;}// 将缓冲区对象绑定到目标gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);// 向缓冲区对象写入数据gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);//获取着色器中attribute变量a_Position的地址 var a_Position = gl.getAttribLocation(gl.program, 'a_Position');if (a_Position < 0) {console.log('Failed to get the storage location of a_Position');return -1;}// 将缓冲区对象分配给a_Position变量gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 9, 0);// 连接a_Position变量与分配给它的缓冲区对象gl.enableVertexAttribArray(a_Position);//获取着色器中attribute变量a_Color的地址 var a_Color = gl.getAttribLocation(gl.program, 'a_Color');if (a_Color < 0) {console.log('Failed to get the storage location of a_Color');return -1;}// 将缓冲区对象分配给a_Color变量gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 9, FSIZE * 3);// 连接a_Color变量与分配给它的缓冲区对象gl.enableVertexAttribArray(a_Color);// 向缓冲区对象分配a_Normal变量,传入的这个变量要在着色器使用才行var a_Normal = gl.getAttribLocation(gl.program, 'a_Normal');if (a_Normal < 0) {console.log('Failed to get the storage location of a_Normal');return -1;}gl.vertexAttribPointer(a_Normal, 3, gl.FLOAT, false, FSIZE * 9, FSIZE * 6);//开启a_Normal变量gl.enableVertexAttribArray(a_Normal);// 将顶点索引写入到缓冲区对象gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);return indices.length;
}

3.2. 改动详解

3.2.1. 设置日照

主要改动是在绘制函数onDraw()中添加了一个设置光照的函数setLight():

//绘制函数
function onDraw(gl, canvas, terrain) {//...//注册鼠标事件initEventHandlers(canvas);//设置灯光setLight(gl);//绘制函数var tick = function () {//...};//开始绘制tick();
}

具体展开这个函数,可以看到这段代码主要是给着色器传入了环境光颜色u_AmbientLight、漫反射光颜色u_DiffuseLight、漫反射光方向u_LightDirection这三个参数。环境光颜色是由其他物体反射照成的,所以环境光强度较弱,设置为(0.2,0.2,0.2)。这里用漫反射光颜色来模拟太阳光,可以设为最强(1.0,1.0,1.0):

//设置灯光
function setLight(gl) {var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');var u_DiffuseLight = gl.getUniformLocation(gl.program, 'u_DiffuseLight');var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');if (!u_DiffuseLight || !u_LightDirection || !u_AmbientLight) {console.log('Failed to get the storage location');return;}//设置漫反射光gl.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0);//...gl.uniform3fv(u_LightDirection, lightDirection.elements);//设置环境光gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
}

前面提到过,太阳光是一种平行光,所以只需要设置方向就行了。这个方向的计算与两个地理学参数太阳高度角solarAltitude和太阳方位角solarAzimuth有关。可以暂时不用去关注其具体的推算细节(可参看我的另外一篇博文通过OSG实现对模型的日照模拟第二节和第四节),只需要知道这里的漫反射方向不是随意指定,是根据实际情况参数计算出来的。

function setLight(gl) {
{//...// 设置光线方向(世界坐标系下的)var solarAltitude = 45.0;var solarAzimuth = 315.0;var fAltitude = solarAltitude * Math.PI / 180; //光源高度角var fAzimuth = solarAzimuth * Math.PI / 180; //光源方位角var arrayvectorX = Math.cos(fAltitude) * Math.cos(fAzimuth);var arrayvectorY = Math.cos(fAltitude) * Math.sin(fAzimuth);var arrayvectorZ = Math.sin(fAltitude);var lightDirection = new Vector3([arrayvectorX, arrayvectorY, arrayvectorZ]);lightDirection.normalize(); // Normalize//...
}

3.2.2. 着色器光照设置

这里顶点着色器中并没有用到传入的光照参数,而是把顶点缓冲区对象的颜色值和法向量值保存为varying变量,用来传入片元缓冲区:

// 顶点着色器程序
var VSHADER_SOURCE ='attribute vec4 a_Position;\n' + //位置'attribute vec4 a_Color;\n' + //颜色'attribute vec4 a_Normal;\n' + //法向量'uniform mat4 u_MvpMatrix;\n' +'varying vec4 v_Color;\n' +'varying vec4 v_Normal;\n' +'void main() {\n' +'  gl_Position = u_MvpMatrix * a_Position;\n' + //设置顶点的坐标'  v_Color = a_Color;\n' +'  v_Normal = a_Normal;\n' +'}\n';

在片元缓冲区中,传入到片元缓冲区的颜色值和法向量值都经过了内插,变成了每个片元的基底色和法向量值。将该法向量归一化,与传入的漫反射方向做点积,得到漫反射入射角。漫反射入射角与传入的漫反射光强度以及片元基底色,根据公式(2)计算漫反射光颜色。片元基底色与传入的环境光颜色,根据公式(1)计算环境反射光颜色。根据公式(3)将两者相加,得到最终显示的片元颜色。

// 片元着色器程序
var FSHADER_SOURCE ='precision mediump float;\n' +'uniform vec3 u_DiffuseLight;\n' + // 漫反射光颜色'uniform vec3 u_LightDirection;\n' + // 漫反射光的方向'uniform vec3 u_AmbientLight;\n' + // 环境光颜色'varying vec4 v_Color;\n' +'varying vec4 v_Normal;\n' +'void main() {\n' +//对法向量归一化'  vec3 normal = normalize(v_Normal.xyz);\n' +//计算光线向量与法向量的点积'  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +//计算漫发射光的颜色 '  vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;\n' +//计算环境光的颜色'  vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +'  gl_FragColor = vec4(diffuse+ambient, v_Color.a);\n' +'}\n';

4. 结果

浏览器最终显示的结果如下:

相比上一篇教程的渲染效果,可以明显发现立体感增强,能够清楚看到地形的起伏情况。

5. 参考

本来部分代码和插图来自《WebGL编程指南》,源代码链接:地址 。会在此共享目录中持续更新后续的内容。

WebGL简易教程(十):光照相关推荐

  1. WebGL简易教程(十四):阴影

    文章目录 1. 概述 2. 示例 2.1. 着色器部分 2.1.1. 帧缓存着色器 2.1.2. 颜色缓存着色器 2.2. 绘制部分 2.2.1. 整体结构 2.2.2. 具体改动 2.2.2.1. ...

  2. WebGL简易教程(十五):加载gltf模型

    文章目录 1. 概述 2. 实例 2.1. 数据 2.2. 程序 2.2.1. 文件读取 2.2.2. glTF格式解析 2.2.2.1. 场景节点 2.2.2.2. 网格 2.2.2.3. 缓冲,缓 ...

  3. WebGL简易教程——目录

    文章目录 1. 绪论 2. 目录 3. 资源 1. 绪论 最近研究WebGL,看了<WebGL编程指南>这本书,结合自己的专业知识写的一系列教程.之前在看OpenGL/WebGL的时候总是 ...

  4. WebGL简易教程(十一):纹理

    文章目录 1. 概述 2. 实例 2.1. 准备纹理 2.2. 配置纹理 2.3. 使用纹理 3. 结果 4. 参考 1. 概述 在之前的之前的教程<WebGL简易教程(九):综合实例:地形的绘 ...

  5. WebGL简易教程(一):第一个简单示例

    文章目录 1. 概述 2. 示例:绘制一个点 1) HelloPoint1.html 2) HelloPoint1.js (1) 准备工作 (2) 着色器 (3) 顶点着色器 (4) 片元着色器 (5 ...

  6. WebGL简易教程(五):图形变换(模型、视图、投影变换)

    文章目录 1. 概述 2. 详论 1) 模型变换 (1) 平移变换 (2) 缩放变换 (3) 旋转变换 (4) 组合变换 2) 视图变换 (1) 原理 (2) 推导 3) 投影变换 (1) 透视投影 ...

  7. Android实战简易教程-第三十九枪(第三方短信验证平台Mob和验证码自动填入功能结合实例)

    用户注册或者找回密码时一般会用到短信验证功能,这里我们使用第三方的短信平台进行验证实例. 我们用到第三方短信验证平台是Mob,地址为:http://mob.com/ 一.注册用户.获取SDK 大家可以 ...

  8. Cesium教程(十四):简易三维模型的可视化

    Cesium教程(十四):简易三维模型的可视化 效果预览 1.高效三维数据格式:3D Tiles 3D Tiles是Cesium提出的处理三维地理大数据的数据格式,目前已是OGC数据标准之一,并在We ...

  9. Android实战简易教程-第五十一枪(ListView实现子控件的动态显示和隐藏、checkbox全选和反选)

    前段时间写过一篇文章:Android实战简易教程-第四十七枪(ListView多选-实现点餐系统)有的同学留言建议,可不可以动态控制checkbox的显示和全选反选功能,我研究了一下,发现实现也比较容 ...

最新文章

  1. Android 人脸识别
  2. RabbitMQ(四)交换机exchange
  3. mysql5.7应该导什么包_立冬为什么要吃饺子?包饺子应该注意什么?怎么煮?看完你就明白...
  4. 法国时隔20年再折桂!“网易云信:世界杯巅峰决战之夜”活动圆满结束!
  5. Roller5.0.3安装配置部署 step by step
  6. linux守护进程的编写
  7. Win7、Ubuntu双系统正确卸载Ubuntu系统
  8. 用编译安装搭建自己的http服务器
  9. Linux系统下的RZSZ(文件传输工具)
  10. python del 函数
  11. SharePoint开发中上传Excel问题 无法更新Microsoft Office文档
  12. thinkphp无法加载控制器:Admin
  13. Windows Phone 7 Jump Start 系列教程
  14. ViewPage2和Fragment以及Tablayout使用
  15. 数学物理方法 数学物理方程
  16. 食物也疯狂!KOOCAN盘点因为食物毁掉的中国电视剧
  17. 【FFT/IDFT】高效算法
  18. 数据分析--分类与回归模型(一)
  19. 前端工程化-基于Taro的Web端Monorepo架构改造
  20. 51单片机中断基本概念

热门文章

  1. 最新最全内隐神经表征论文合集
  2. 集成学习2:Boosting算法:AdaboostGBDT
  3. vue表情包公共组件(适用于聊天室)
  4. 又一重磅内容|海外现金贷产品形态及风控措施
  5. 【C++】C++操作jsoncpp(写、读、解析)+jsoncpp从0到1配置步骤
  6. 米家扫地机器人重置网络_米家扫地机器人 怎么清理传感器
  7. 荣耀笔记本MagicBook Pro 体验~学生族走过路过别错过
  8. 亚洲信贷监察:数据分析让应收账款管理更智能
  9. vue.js AEC代码编辑器
  10. 阿里巴巴-easyexcel 下载案例