zoukankan      html  css  js  c++  java
  • WebGL简易教程(十):光照

    1. 概述

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

    2. 原理

    2.1. 光源类型

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

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

    如图所示:
    image

    2.2. 反射类型

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

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

    环境反射是针对环境光而言的,在环境反射中,环境光照射物体是各方面均匀、强度相等的,反射的方向可以认为就是入射光的反方向。也就是最终物体的颜色只跟入射光颜色和基底色有关。那么可以这样定义环境反射光颜色:

    [<环境反射光颜色>=<入射光颜色>×<表面基底色> ag{1} ]

    注意在式子中,这个乘法操作指的是颜色矢量上逐分量相乘。

    2.2.2. 漫反射(diffuse reflection)

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

    漫反射中,反射光的颜色除了取决于入射光的颜色、表面的基底色,还有入射光与物体表面的法向量形成的入射角。令入射角为θ,漫反射光的颜色可以根据下式计算:

    [<漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ ag{2} ]

    入射角θ可以通过矢量的点积来计算:

    [<光线方向>·<法线方向> = |光线方向|*|法线方向|*cosθ ]

    如果光线方向和法线方向都是归一化的,那么向量的模(长度)就为1,则有:

    [<漫反射光颜色>=<入射光颜色>×<表面基底色>×(<光线方向>·<法线方向>) ]

    注意,这里的“光线方向”,实际上指的是入射方向的反方向,即从入射点指向光源方向,如图所示:
    image

    2.2.3. 综合

    当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色:

    [<表面的反射光颜色> = <漫反射光颜色>+<环境反射光颜色> ag{3} ]

    3. 实例

    3.1. 具体代码

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

    // 顶点着色器程序
    var VSHADER_SOURCE =
      'attribute vec4 a_Position;
    ' + //位置
      'attribute vec4 a_Color;
    ' + //颜色
      'attribute vec4 a_Normal;
    ' + //法向量
      'uniform mat4 u_MvpMatrix;
    ' +
      'varying vec4 v_Color;
    ' +
      'varying vec4 v_Normal;
    ' +
      'void main() {
    ' +
      '  gl_Position = u_MvpMatrix * a_Position;
    ' + //设置顶点的坐标
      '  v_Color = a_Color;
    ' +
      '  v_Normal = a_Normal;
    ' +
      '}
    ';
    
    // 片元着色器程序
    var FSHADER_SOURCE =
      'precision mediump float;
    ' +
      'uniform vec3 u_DiffuseLight;
    ' + // 漫反射光颜色
      'uniform vec3 u_LightDirection;
    ' + // 漫反射光的方向
      'uniform vec3 u_AmbientLight;
    ' + // 环境光颜色
      'varying vec4 v_Color;
    ' +
      'varying vec4 v_Normal;
    ' +
      'void main() {
    ' +
      //对法向量归一化
      '  vec3 normal = normalize(v_Normal.xyz);
    ' +
      //计算光线向量与法向量的点积
      '  float nDotL = max(dot(u_LightDirection, normal), 0.0);
    ' +
      //计算漫发射光的颜色 
      '  vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;
    ' +
      //计算环境光的颜色
      '  vec3 ambient = u_AmbientLight * v_Color.rgb;
    ' +
      '  gl_FragColor = vec4(diffuse+ambient, v_Color.a);
    ' +
      '}
    ';
    
    //定义一个矩形体:混合构造函数原型模式
    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);
    
        //请求浏览器调用tick
        requestAnimationFrame(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(); // Normalize
      gl.uniform3fv(u_LightDirection, lightDirection.elements);
    
      //设置环境光
      gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
    }
    
    //读取DEM函数
    function readDEMFile(result, terrain) {
      var stringlines = result.split("
    ");
      if (!stringlines || stringlines.length <= 0) {
        return false;
      }
    
      //读取头信息
      var subline = stringlines[0].split("	");
      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 not
      var 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 ratio
          var 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_MvpMatrix
      var 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_MvpMatrix
      gl.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;
    ' + //位置
      'attribute vec4 a_Color;
    ' + //颜色
      'attribute vec4 a_Normal;
    ' + //法向量
      'uniform mat4 u_MvpMatrix;
    ' +
      'varying vec4 v_Color;
    ' +
      'varying vec4 v_Normal;
    ' +
      'void main() {
    ' +
      '  gl_Position = u_MvpMatrix * a_Position;
    ' + //设置顶点的坐标
      '  v_Color = a_Color;
    ' +
      '  v_Normal = a_Normal;
    ' +
      '}
    ';
    

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

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

    4. 结果

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

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

    5. 参考

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

  • 相关阅读:
    NBUT 1120 Reimu's Teleport (线段树)
    NBUT 1119 Patchouli's Books (STL应用)
    NBUT 1118 Marisa's Affair (排序统计,水)
    NBUT 1117 Kotiya's Incantation(字符输入处理)
    NBUT 1115 Cirno's Trick (水)
    NBUT 1114 Alice's Puppets(排序统计,水)
    188 Best Time to Buy and Sell Stock IV 买卖股票的最佳时机 IV
    187 Repeated DNA Sequences 重复的DNA序列
    179 Largest Number 把数组排成最大的数
    174 Dungeon Game 地下城游戏
  • 原文地址:https://www.cnblogs.com/charlee44/p/11668014.html
Copyright © 2011-2022 走看看