WebGL笔记:图形转面的原理与实现

news2024/11/21 0:17:16

1 )回顾 WebGL 三种面的适应场景

  • TRIANGLES 单独三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇
  • 备注
    • 在实际的引擎开发中,TRIANGLES 是用得最多的
    • TRIANGLES 的优势是可以绘制任意模型,缺点是比较费点

2 )适合 TRIANGLES 单独三角形的的模型


  • 比如上述这个不规则图形

3 )TRIANGLE_STRIP 和TRIANGLE_FAN 的优点和缺点

  • TRIANGLE_STRIP 和TRIANGLE_FAN 的优点是相邻的三角形可以共用一条边,比较省点
  • 然而其缺点也太明显,因为它们只适合绘制具备相应特点的模型
  • 适合TRIANGLE_STRIP三角带的模型如下

  • 适合TRIANGLE_FAN三角扇的模型如下

  • 扩展:
    • three.js 使用的绘制面的方式就是TRIANGLES,可以在其 WebGLRenderer 对象的源码的 renderBufferImmediate 方法中查看
      _gl.drawArrays(_gl.TRIANGLES, 0, object.count);
      

4 )图形转面的基本步骤

  • 在three.js 里有一个图形几何体ShapeGeometry,可以把图形变成面。

  • 只要有数学支撑,就可以实现这种效果

5 )使用TRIANGLES 独立三角形的方式,将图形转成面

  • 原理

    • 使用的方法叫做“砍角”
    • 其原理就是从起点将多边形中符合特定条件的角逐个砍掉,然后保存到一个集合里
    • 直到把多边形砍得只剩下一个三角形为止。
    • 这时候集合里的所有三角形就是我们想要的独立三角形。
  • 举个例子


  • 已知:逆时针绘图的路径G

  • 求:将其变成下方网格的方法


  • 求解步骤
    • 1)寻找满足以下条件的 ▲ABC
      • ▲ABC 的顶点索引位置连续,如:012、123、234
      • 比如,012时,A是0,B是1,C是2
      • 点C在向量AB的正开半平面里,可以理解为你站在A点,面朝B点,点C要在你的左手边
      • ▲ABC 中没有包含 路径G 中的其它顶点
    • 2)当找到 ▲ABC 后,就将点B从路径的顶点集合中删掉,并将ABC三个点存入集合中,然后继续往后找。
    • 3)当路径的定点集合只剩下3个点时,就结束。
    • 4)由所有满足条件的 ▲ABC 构成的集合就是我们要求的独立三角形集合。

  • 在上述坐标系示例中,可辅助我们找到C点
  • A是起点,B是第二个点,C则是第三个点,用这个方法,可以看到C点是否在向量AB(A->B)的左手边
  • 砍角的精髓就在于不断消耗B,让图形可砍,逐渐砍完,比如B点是5时满足条件,5被干掉,下一轮砍角就可组 4,6,7 这个三角形
  • 每一轮都是按照顺序(不一定是升序,因为可能从最后一个点x,0,1这样组)找三个点组成一个三角形,判断这个三角形是否符合被砍的标准

  • 上面这个图形, 进一步理解如何判断一个点,比如B,在一个向量OA的左侧还是右侧
    • 从 OA 转到 OB 需要 θ \theta θ 度,如果 这个角度大于零,则B在OA的左侧,反之,则在右侧
    • 如何判断 这个 θ \theta θ 是 大于零 还是 小于零, 有一个算法可以使用:
      • s i n θ ∗ ∣ O A ∣ ∗ ∣ O B ∣ = a x ∗ b y − a y ∗ b x sin \theta * |OA| * |OB| = ax * by - ay * bx sinθOAOB=axbyaybx
      • 在这个公式中,|OA|, |OB| 都是绝对值,都是 >=0 的,所以,只要后面的 a x ∗ b y − a y ∗ b x > 0 ax * by - ay * bx > 0 axbyaybx>0, 则 s i n θ > 0 sin \theta > 0 sinθ>0, θ > 0 \theta > 0 θ>0
      • 这样就可判断出一个点在向量的哪一侧了

图形转面的实现

1 )绘制路径G

  • 路径G的顶点数据
    const pathData = [
      0, 0,
      600, 0,
      600, 100,
      100, 100,
      100, 500,
      500, 500,
      500, 300,
      300, 300,
      300, 400,
      200, 400,
      200, 200,
      600, 200,
      600, 600,
      0, 600
    ];
    
    • 这一些数据是CSS的顶点数据,走的是CSS的坐标系
    • 画CSS的时候,需要逆时针绘图
    • 在CSS坐标系,水平的是 x轴(向右为正),垂直的是 y轴(向下为正), 原点在左上角

  • 在上图中,就是数据对应的图形,原点 (0, 0) 对应左上角,逆时针绘制出来的
  • 在pathData里两个数字为一组,分别代表顶点的x位和y位
  • pathData里的数据是我以像素为单位画出来的,在实际项目协作中,UI给我们的svg文件可能也是以像素为单位画出来的,这个我们要做好心理准备
  • 因为,webgl画布的宽和高永远都是两个单位
  • 所以,我们要将上面的点画到webgl 画布中,就需要做一个数据映射

2 )在webgl 中绘制正方形

  • 从pathData 数据中我们可以看出,路径G的宽高都是600,是一个正方形。

  • 所以,我可以将路径G映射到webgl 画布的一个正方形中。

  • 这个正方形的高度我可以暂且定为1,那么其宽度就应该是高度除以canvas画布的宽高比。

    //宽高比
    const ratio = canvas.width / canvas.height;
    // 正方形高度 这里 1 实际上是整个canvas画布高度的一半
    const rectH = 1.0;
    // 正方形宽度 (这里是由宽高比计算出来的) 在webgl坐标系上宽度上一个单位和高度上一个单位可能是不一样的,是根据canvas画布来走的
    const rectW = rectH / ratio;
    

3 )正方形的定位,把正方形放在webgl画布的中心

  • 获取正方形尺寸的一半,然后求出其x、y方向的两个极值即可。

    //正方形宽高的一半
    const [halfRectW, halfRectH] = [rectW / 2, rectH / 2];
    //两个极点
    const minX = -halfRectW;
    const minY = -halfRectH;
    const maxX = halfRectW;
    const maxY = halfRectH;
    

4 )利用之前的Poly对象绘制正方形,测试一下效果

// 这个 Poly 对象的实现参考前面webgl博文
const rect = new Poly({
    gl,
    vertices: [
        minX, maxY,
        minX, minY,
        maxX, minY, 
        maxX, maxY,
    ],
});
rect.draw();

  • 先画了4个点,效果没问题之后,就可以把数据在这个正方形里做映射了

5 )建立x轴和y轴比例尺

const scaleX = ScaleLinear(0, minX, 600, maxX);
const scaleY = ScaleLinear(0, minY, 600, maxY);
function ScaleLinear(ax, ay, bx, by) {
  const delta = {
    x: bx - ax,
    y: by - ay,
  };
  const k = delta.y / delta.x;
  const b = ay - ax * k;
  return function (x) {
    return k * x + b;
  };
}
  • ScaleLinear 这个方法的实现参考下文中的全部代码,是对点斜式做了一个封装,内部参数是两种坐标数据的极小值和极大值
    ScaleLinear(
      0, minX,
      600, maxX
    );
    
    • 参数第一列是 css 数据; 第二列是 webgl 数据
    • 参数第一行是 两种坐标数据的极小值; 第二行是 两种坐标数据的极大值
  • ScaleLinear(ax, ay, bx, by) 方法使用的就是点斜式,用于将x轴和y轴上的数据像素数据映射成 webgl数据
    • ax 像素数据的极小值
    • ay webgl数据的极小值
    • bx 像素数据的极大值
    • by webgl数据的极大值

6 )将路径G中的像素数据解析为 webgl 数据

const glData = [];
for (let i = 0; i < pathData.length; i += 2) {
    // 把每个点做一个解析,并存入集合中
    glData.push(scaleX(pathData[i]), scaleY(pathData[i + 1]));
}
const path = new Poly({
    gl,
    vertices: glData,
    types: ["POINTS", "LINE_LOOP"],
});
path.draw();

7 )将图形网格化,把图形变成面


  • 建立了一个ShapeGeo 对象,用于将图形网格化

    const shapeGeo = new ShapeGeo(glData)
    
  • ShapeGeo.js

    export default class ShapeGeo {
      constructor(pathData = []) {
        this.pathData = pathData; // 路径数据 平铺展开
        this.geoData = []; // 将路径数据 转换成 对象型数组 方便操作
        this.triangles = []; // 存储的三角形集合
        this.vertices = []; // 平铺展开的 独立三角形数据,后面会交给缓冲区进行渲染
        this.parsePath(); // 将 pathData 转换成 geoData
        this.update(); // 更新方法,默认执行
      }
      update() {
        this.vertices = [];
        this.triangles = [];
        this.findTriangle(0); // 寻找独立三角形
        this.upadateVertices() // 从独立三角形区域解析出 vertices
      }
      parsePath() {
        this.geoData = [];
        const { pathData, geoData } = this
        for (let i = 0; i < pathData.length; i += 2) {
          geoData.push({ x: pathData[i], y: pathData[i + 1] })
        }
      }
      findTriangle(i) {
        const { geoData, triangles } = this;
        const len = geoData.length;
        if (geoData.length <= 3) {
          triangles.push([...geoData]);
        } else {
          // 对位置做了一个加工,因为如果从最后一个点开始找,最后一个点的下一个肯定是第一个,这里对顶点长度进行取余操作,循环进行
          const [i0, i1, i2] = [
            i % len,
            (i + 1) % len,
            (i + 2) % len
          ];
          const triangle = [
            geoData[i0],
            geoData[i1],
            geoData[i2],
          ];
          // 判断找的三角形是否符合条件:在左手边 && 在三角形中是否有点
          if (this.cross(triangle) > 0 && !this.includePoint(triangle)) {
            triangles.push(triangle); // 存储三角形
            geoData.splice(i1, 1); // 移除 B点 (第二个点)
          }
          this.findTriangle(i1); // 接着再从第二个点开始寻找, 这里即便第二个点被删了,之前的i2也会是这里的i1
        }
      }
      includePoint(triangle) {
        // 遍历所有顶点
        for (let ele of this.geoData) {
          // 判断当前三角形是否包含当前点,如果否
          if (!triangle.includes(ele)) {
            // 判断其他点是否在这个三角形之中
            if (this.inTriangle(ele, triangle)) {
              return true;
            }
          }
        }
        return false;
      }
      inTriangle(p0, triangle) {
        let inPoly = true;
        // 遍历三角形点
        for (let i = 0; i < 3; i++) {
          const j = (i + 1) % 3; // j 是下一个点,这里 余3运算 和上面一样的原理
          const [p1, p2] = [triangle[i], triangle[j]];
          // p0 点是否在三角形每一条边的 右侧 或 左侧
          if (this.cross([p0, p1, p2]) < 0) {
            inPoly = false;
            break
          }
        }
        return inPoly;
      }
      // 这个就是这面讲到的 θ > 0的原理中,只要右边 > 0了,θ 就 > 0
      cross([p0, p1, p2]) {
        const [ax, ay, bx, by] = [
          p1.x - p0.x,
          p1.y - p0.y,
          p2.x - p0.x,
          p2.y - p0.y,
        ];
        return ax * by - bx * ay;
      }
      upadateVertices() {
        const arr = []
        this.triangles.forEach(triangle => {
          for (let { x, y } of triangle) {
            arr.push(x, y)
          }
        })
        this.vertices = arr
      }
    }
    
  • 属性

    • pathData 平展开的路径数据
    • geoData 由路径数据pathData 转成的对象型数组
    • triangles 三角形集合,对象型数组
    • vertices 平展开的对立三角形顶点集合
  • 方法

    • update() 更新方法,基于pathData 生成vertices
    • parsePath() 基于路径数据pathData 转成对象型数组
    • findTriangle(i) 寻找符合条件的三角形
      • i 顶点在geoData 中的索引位置,表示从哪里开始寻找三角形
    • includePoint(triangle) 判断三角形中是否有其它顶点
    • inTriangle(p0, triangle) 判断一个顶点是否在三角形中
    • cross([p0, p1, p2]) 以p0为基点,对二维向量p0p1、p0p2做叉乘运算
    • upadateVertices() 基于对象数组geoData 生成平展开的 vertices 数据
  • 绘制G形面

    const face = new Poly({
        gl,
        vertices: shapeGeo.vertices,
        types: ["TRIANGLES"],
    });
    face.draw();
    

核心代码

<canvas id="canvas"></canvas>
  <!-- 顶点着色器 -->
  <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      void main() {
          gl_Position = a_Position;
          gl_PointSize = 10.0;
      }
    </script>
  <!-- 片元着色器 -->
  <script id="fragmentShader" type="x-shader/x-fragment">
      void main(){
          gl_FragColor = vec4(1,1,0,1);
      }
    </script>
  <script type="module">
    import { initShaders, ScaleLinear } from "./utils/utils.js";
    import Poly from "../utils/Poly.js";
    import ShapeGeo from "../utils/ShapeGeo.js";

    const canvas = document.querySelector("#canvas");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // 获取着色器文本
    const vsSource = document.querySelector("#vertexShader").innerText;
    const fsSource = document.querySelector("#fragmentShader").innerText;

    //三维画笔
    const gl = canvas.getContext("webgl");

    //初始化着色器
    initShaders(gl, vsSource, fsSource);

    //声明颜色 rgba
    gl.clearColor(0, 0, 0, 1);
    //刷底色
    gl.clear(gl.COLOR_BUFFER_BIT);

    //路径G-逆时针
    const pathData = [
      0, 0,
      0, 600,
      600, 600,
      600, 200,
      200, 200,
      200, 400,
      300, 400,
      300, 300,
      500, 300,
      500, 500,
      100, 500,
      100, 100,
      600, 100,
      600, 0,
    ];

    //宽高比
    const ratio = canvas.width / canvas.height;
    //正方形高度
    const rectH = 1.0;
    //正方形宽度
    const rectW = rectH / ratio;
    //正方形宽高的一半
    const [halfRectW, halfRectH] = [rectW / 2, rectH / 2];
    //两个极点
    const minX = -halfRectW;
    const minY = -halfRectH;
    const maxX = halfRectW;
    const maxY = halfRectH;
    //正方形
    const rect = new Poly({
      gl,
      vertices: [
        minX, maxY,
        minX, minY,
        maxX, minY,
        maxX, maxY,
      ],
    });
    rect.draw();


    //建立比例尺
    const scaleX = ScaleLinear(
      0, minX,
      600, maxX
    );
    const scaleY = ScaleLinear(
      0, maxY,
      600, minY
    );

    //将路径G中的像素数据解析为webgl数据
    const glData = [];
    for (let i = 0; i < pathData.length; i += 2) {
      glData.push(scaleX(pathData[i]), scaleY(pathData[i + 1]));
    }
    const path = new Poly({
      gl,
      vertices: glData,
      types: ["POINTS", "LINE_LOOP"],
    });
    path.draw();

    const shapeGeo = new ShapeGeo(glData)
    const face = new Poly({
      gl,
      vertices: shapeGeo.vertices,
      types: ["TRIANGLES"],
    });
    face.draw();
  </script>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1119991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Apache JMeter 安装教程

下载&#xff1a; 注意事项&#xff1a;使用JMeter前需要配置JDK环境 下载地址 下载安装以后&#xff0c;打开安装的bin目录 D:\software\apache-jmeter-5.4.1\apache-jmeter-5.4.1\bin&#xff0c;找到jmeter.bat&#xff0c;双击打开 打开后的样子 语言设置&#xff1a; 1…

windows模拟触摸

安装EcoTUIODriver驱动 GitHub - almighty-bungholio/EcoTUIODriver: Diver to convert tuio touch events into windows touch events. Started as GSoC 2012 project. 安装完后电脑属性显示笔和触控为为20点触摸点提供笔和触控支持。 在另一台电脑上运行tuio模块器是一个ja…

【iOS】JSON解析

JSON在Web开发和网络通信和传输中广泛应用&#xff0c;常用于存储和传输数据&#xff0c;这些数据一般也都是JSON格式&#xff0c;可以说绝大多数网络请求传输的数据都是JSON格式 在之前有关网络请求文章中&#xff0c;实现了网络数据加载流程&#xff0c;并对加载下来的JSON数…

Es集群部署

目录 组件全家套 版本说明 主机准备 1.解压安装 2.运行环境配置 2.1修改每个节点linux系统限制 2.2 修改每个节点 linux 系统配置 2.3 调整vm.max_map_count的大小 2.4 重启验证配置 3. 配置ES 3.1 每个节点创建ES用户&#xff0c;ES不能使用root启动 3.2 每个节点…

ELK概述部署和Filebeat 分布式日志管理平台部署

ELK概述部署、Filebeat 分布式日志管理平台部署 一、ELK 简介二、ELK部署2.1、部署准备2.2、优化elasticsearch用户拥有的内存权限2.3、启动elasticsearch是否成功开启2.4、浏览器查看节点信息2.5、安装 Elasticsearch-head 插件2.6、ELK Logstash 部署&#xff08;在 Apache 节…

数据库的基本知识理论

文章目录 一、数据库的演变史1.存取数据的演变史1.把数据存在了文件中2.存数据的文件越来越多&#xff0c;放在db文件夹3.数据库软件就能够解决以上所有问题 2.数据库软件应用史1.单机游戏2.网络游戏3. 集群 二、数据库1.什么是数据库2.数据库的作用1.实现数据共享2.减少数据的…

【Android知识笔记】Webview专题

WebView 核心组件 类名作用常用方法WebView创建对象加载URL生命周期管理状态管理loadUrl():加载网页 goBack():后退WebSettings配置&管理 WebView缓存:setCacheMode() 与JS交互:setJavaScriptEnabled()WebViewClient处理各种通知&请求事件should

系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第三部分:缓存

本心、输入输出、结果 文章目录 系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第三部分&#xff1a;缓存前言缓存数据存储在什么地方图层说明 为什么 Redis 这么快&#xff1f;如何使用 Redis顶级缓存策略弘扬爱国精神 系统设计 - 我们如何通俗的理解那些技术的运行原理…

嵌入式硬件中常见的100种硬件选型方式

1请列举您知道的电阻、电容、电感品牌&#xff08;最好包括国内、国外品牌&#xff09;。 电阻&#xff1a; 美国&#xff1a;AVX、VISHAY 威世 日本&#xff1a;KOA 兴亚、Kyocera 京瓷、muRata 村田、Panasonic 松下、ROHM 罗姆、susumu、TDK 台湾&#xff1a;LIZ 丽智、PHY…

SystemVerilog学习(4)——自定义结构

一、 通过typedef来创建用户自定义类型 typedef语句可以用来创建新的类型。例如,你要求一个算术逻辑单元(ALU)在编译时可配置,以适应8比特、16比特,24比特或32比特等不同位宽的操作数。在Verilog中,你可以为操作数的位宽和类型分别定义一个宏(macro),如例2.32所示。 SV则提供了…

Linux常用命令——clockdiff命令

在线Linux命令查询工具 clockdiff 检测两台linux主机的时间差 补充说明 在ip报文的首部和ICMP报文的首部都可以放入时间戳数据。clockdiff程序正是使用时间戳来测算目的主机和本地主机的系统时间差。 选项 -o&#xff1a;使用IP时间戳选项来测量系统时间差。时间戳只用3个…

超全全国所有城市人力资本测算数据集(1990-2021年)

参考《管理世界》中詹新宇&#xff08;2020&#xff09;的做法&#xff0c;本文对地级市的人力资本水平进行测算&#xff0c;其中人力资本水平用地级市的普通高等学校在校学生数占该地区总人口比重来反映 一、数据介绍 数据名称&#xff1a;地级市-人力资本测算 数据年份&…

基于指数分布优化的BP神经网络(分类应用) - 附代码

基于指数分布优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于指数分布优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.指数分布优化BP神经网络3.1 BP神经网络参数设置3.2 指数分布算法应用 4.测试结果…

计算一个Series序列元素的标准差Series.sem()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 计算Series序列中各元素的标准差 Series.sem() [太阳]选择题 关于以下代码的说法中正确的是? import pandas as pd a pd.Series([1,2,3]) print("【显示】a:\n",a) print(【执行】…

电路基础元件

文章目录 每周电子w5——电路元件基本电路元件电阻元件电容元件电感元件 每周电子w5——电路元件 基本电路元件 电路元件&#xff1a;是电路中最基本的组成单元 电路元件通过其端子与外部相连接&#xff1b;元件的特性则通过与端子有关的物理量描述每一种元件反映某种确定的电…

v-if和v-else-if、v-else或v-show

一、用于真正的隐藏和显示&#xff0c;显示时才会渲染dom元素 v-if"布尔类型条件"&#xff1a;能出现n次 v-else-if"布尔类型条件":必须与v-if搭配使用&#xff0c;出现n次 v-else&#xff1a;必须和v-if搭配使用&#xff0c;只能出现1次 例子1&#xff1…

LeetCode讲解篇之113. 路径总和 II

文章目录 题目描述题解思路题解代码 题目描述 题解思路 深度优先遍历二叉树&#xff0c;遍历的同时记录路径&#xff0c;直到遍历到叶节点&#xff0c;若路径和为targetSum则添加到结果集中 题解代码 func pathSum(root *TreeNode, targetSum int) [][]int {var res make([…

win10下yolov5 tensorrt模型部署

TensorRT系列之 Win10下yolov8 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov8 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov7 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov6 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov5 tensorrt模型加速部署…

云原生概述

1. 何谓云原生 云原生是一种构建和运行应用程序的方法&#xff0c;是一套技术体系和方法论。云原生&#xff08;CloudNative&#xff09;是一个组合词&#xff0c;CloudNative。Cloud表示应用程序位于云中&#xff0c;而不是传统的数据中心&#xff1b;Native表示应用程序从设…

算法学习 之 并查集

leecode 中这题就需要并查集 代码如下 typedef struct{int * parents;int * sizes; } UnionFind; // 下面建立一个初始化 UnionFind * NewUnionFind(int n){UnionFind * uf (UnionFind *) malloc(sizeof(UnionFind));uf->parents (int *) malloc (sizeof(int)*n);uf->s…