feng 发布的文章

WEBGL实现带数字的圆点

废话不说,代码如下

// 绘制带数字的圆点
import {
  Scene,
  BufferGeometry,
  Points,
  PerspectiveCamera,
  Vector3,
  WebGLRenderer,
  CanvasTexture,
  ShaderMaterial,
  Color,
  BufferAttribute,
} from "three";

let renderer, scene, camera;
const width = window.innerWidth;
const height = window.innerHeight;

function init() {
  const container = document.getElementById("app");
  scene = new Scene();

  camera = new PerspectiveCamera(45, width / height, 1, 10000);
  camera.position.set(0, 0, 18);
  camera.lookAt(scene.position);
  camera.updateMatrix();

  const points = createPoint();
  scene.add(points);

  // const helper = new AxesHelper(5);
  // scene.add(helper);

  renderer = new WebGLRenderer();
  renderer.setSize(width, height);

  container.appendChild(renderer.domElement);
}

function createPoint() {
  const geometry = new BufferGeometry().setFromPoints([
    new Vector3(-3, 3, 0),
    new Vector3(0, 3, 0),
    new Vector3(3, 3, 0),

    new Vector3(-3, 0, 0),
    new Vector3(0, 0, 0),
    new Vector3(3, 0, 0),

    new Vector3(-3, -3, 0),
    new Vector3(0, -3, 0),
    new Vector3(3, -3, 0),

    new Vector3(0, 6, 0),
  ]);

  const numbers = [1, 2, 3, 4, 56789, 6, 7, 768, 19, 0];

  geometry.setAttribute(
    "a_number",
    new BufferAttribute(new Float32Array(numbers), 1)
  );

  const material = new createMaterial();
  const points = new Points(geometry, material);
  return points;
}

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

init();
animate();
// testAppendCanvas();

function createCanvasSpirit() {
  const SIZE = 64;
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");

  canvas.width = SIZE * 10;
  canvas.height = SIZE;

  ctx.font = SIZE + "px serif";
  ctx.fillStyle = "#000";
  // 居中
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";

  for (let i = 0; i < 10; i++) {
    ctx.fillText(i, SIZE * i + SIZE / 2, SIZE / 2);
  }

  return canvas;
}

function testAppendCanvas() {
  const canvas = createCanvasSpirit();
  canvas.style.border = "1px solid #000";
  canvas.style.marginTop = "10px";
  canvas.style.marginLeft = "10px";
  const container = document.getElementById("app");
  container.appendChild(canvas);
}

function createCanvasTexture() {
  const canvas = createCanvasSpirit();
  const texture = new CanvasTexture(canvas);
  return texture;
}

function createMaterial() {
  const vertexShader = `
    attribute float a_number;
    uniform float u_size;
    varying float v_number;


    void main(){
      v_number = a_number;

      gl_PointSize = u_size;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
  `;
  const fragmentShader = `
    uniform vec3 u_color;
    uniform vec3 u_front_color;
    uniform sampler2D u_number_spirit;

    varying float v_number;
    float numDigits(float num) {
      float digits = 1.0;
      while (num >= 10.0) {
        num = floor( num / 10.0 );
        digits += 1.0;
      }
      return floor(digits);
    }

    float getNumByDigits(float num, float digits) {
      float digit = floor( mod(num / pow(10.0, digits ), 10.0) );
      return digit;
    }

    void main() {
      float dist = distance( gl_PointCoord, vec2(0.5,0.5) );
      float discard_opacity =  1.0 - smoothstep( 0.48, 0.5, dist );
      if( discard_opacity == 0.0 ) discard;

      vec2 uv = vec2( gl_PointCoord.x,1.0- gl_PointCoord.y);

      float index = mod(v_number, pow(10.0,MAX_DIGITS) );
      float num_length = numDigits(index); // 数字长度

      uv = clamp((uv - 0.5) * (0.8 + num_length * 0.2) + 0.5,0.0,1.0);

      float num_step = 1.0 / num_length; // 每位数字占的宽度
      float current_digits = floor( uv.x / num_step ); // 当前uv渲染的是第几个数字

      float value = getNumByDigits(index, num_length - 1.0 - current_digits);  // 需要渲染的对应数字

      float x = mod(uv.x,num_step)/num_step * 0.1;
      if(num_length > 1.0){
        x = x/2.0 +  value * 0.1 + 0.025;
      }else{
        x += value * 0.1;
      }
      float y = uv.y;

      float opacity = texture2D( u_number_spirit, vec2(x,y)).a;
      opacity = step(0.1,opacity) * opacity;

      vec3 color = mix(u_color, u_front_color, opacity);

      vec4 diffuseColor = vec4( color, 1.0 * discard_opacity );
      gl_FragColor = diffuseColor;
    }

  `;

  const texture = createCanvasTexture();
  const size = 64 * window.devicePixelRatio;
  const material = new ShaderMaterial({
    uniforms: {
      u_size: { value: size }, // 点的大小
      u_color: { value: new Color(0x00ff00) }, // 背景填充色
      u_front_color: { value: new Color(0xff0000) }, // 文字颜色
      u_number_spirit: { value: texture }, // 数字纹理
    },
    defines: {
      MAX_DIGITS: "3.0", // 最大支持3位数
    },
    vertexShader,
    fragmentShader,
    transparent: true,
  });
  return material;
}

WEBGL中使用shader实现旋转的圆环

1.实现圆环绘制

// fragmentshader
void main() {
  float radius = 0.5;
  vec2 st = gl_PointCoord.xy;
  float dist = distance(st,vec2(0.5,0.5));

  float alpha = smoothstep(radius-0.12,radius,dist) - smoothstep(radius-0.1,radius,dist);
  gl_FragColor = vec4( 1.0,0.0,0.0, alpha);
}

2.让圆环存在一个1/4的缺口(具体大小可以自己调整)

// fragmentshader
#define PI 3.14159265359
void main() {
  float radius = 0.5;
  vec2 st = gl_PointCoord.xy;
  float dist = distance(st,vec2(0.5,0.5));

  float alpha = smoothstep(radius-0.12,radius,dist) - smoothstep(radius-0.1,radius,dist);
  float theta = mod( atan(st.y - 0.5,st.x - 0.5) + PI , PI*2.0);
  float a = step( PI/2.0, theta );
  gl_FragColor = vec4( 1.0,0.0,0.0, alpha*a);
}

完整html的demo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <script type="x-shader/x-vertex" id="vertexshader">
attribute float aSize;
attribute float aStartAngle;

varying float vStartAngle;

void main() {
  vStartAngle = aStartAngle;
  gl_PointSize = aSize;

  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
    </script>

    <script type="x-shader/x-fragment" id="fragmentshader">
uniform float uTheta;
varying float vStartAngle;
void main() {
  float radius = 0.5;
  vec2 st = gl_PointCoord.xy;
  float dist = distance(st,vec2(0.5,0.5));

  float alpha = smoothstep(radius-0.15,radius,dist) - smoothstep(radius-0.05,radius,dist);
  float theta = mod( atan(st.y - 0.5,st.x - 0.5) + PI + vStartAngle + uTheta , PI*2.0);
  float a = step( PI/2.0, theta );
  gl_FragColor = vec4( 1.0,0.0,0.0, alpha*a);
}
    </script>
    <script type="module">
      import * as THREE from "https://unpkg.com/three@0.155.0/build/three.module.js";
      // import * as THREE from './three.module.js'

      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );

      const renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.body.appendChild(renderer.domElement);

      const geometry = new THREE.BufferGeometry();
      const positions = [];
      const sizes = [];
      const startAngles = [];
      for (let i = 0; i < 10; i++) {
        // position
        for (let s = 0; s < 3; s++) {
          positions.push(Math.random() * 5 - 2.5);
        }

        // size
        sizes.push(20 + Math.random() * 30);

        // startAngle 让每个圆环不要看起来是同步的
        startAngles.push(Math.random() * Math.PI * 2);
      }

      geometry.setAttribute(
        "position",
        new THREE.Float32BufferAttribute(positions, 3)
      );
      geometry.setAttribute(
        "aSize",
        new THREE.Float32BufferAttribute(sizes, 1)
      );
      geometry.setAttribute(
        "aStartAngle",
        new THREE.Float32BufferAttribute(startAngles, 1)
      );

      const material = new THREE.ShaderMaterial({
        vertexShader: document.getElementById("vertexshader").textContent,
        fragmentShader: document.getElementById("fragmentshader").textContent,
        uniforms: {
          uTheta: { value: (Date.now() * 0.01) % (Math.PI * 2) },
        },
        defines: {
          PI: Math.PI,
        },
        transparent: true,
      });

      const points = new THREE.Points(geometry, material);

      scene.add(points);

      camera.position.z = 5;

      function animate() {
        requestAnimationFrame(animate);

        material.uniforms.uTheta.value = (Date.now() * 0.01) % (Math.PI * 2);

        renderer.render(scene, camera);
      }

      animate();
    </script>
  </body>
</html>

https://pic1.zhimg.com/v2-4d47e7de914f256f2035482338aa4244_b.webp

旋转椭圆外包围盒计算

前段时间实现了一下椭圆工具的编辑工具,里面涉及到计算椭圆的外包围盒.隐隐觉得椭圆应该有计算外包围盒的数学方法,可惜问GPT4永远都是循环360度去算边缘返回外包围盒.要不就是按照正椭圆的方式来算外包围盒.
昨天认真搜索了一下竟然真搜索到了计算方法.(果然gpt还是没法完全替代搜索引擎的O(∩_∩)O哈哈~)

附上参考链接
https://cloud.tencent.com/developer/article/2067487?from=15425&areaSource=102001.10&traceId=Wd9aL_9AgUh-MndIu61Dv

再附上自己写的js实现

// centerX,centerY 椭圆中心点,a 长轴,b 短轴, theta 弧度
export function getEllipseBoundingBox(centerX: number, centerY: number, a: number, b: number, theta: number) {
  const sin_theta = Math.sin(theta)
  const cos_theta = Math.cos(theta)

  const A = a ** 2 * sin_theta ** 2 + b ** 2 * cos_theta ** 2
  const B = 2 * (a ** 2 - b ** 2) * sin_theta * cos_theta
  const C = a ** 2 * cos_theta ** 2 + b ** 2 * sin_theta ** 2
  const D = -(a ** 2 * b ** 2)

  const h = Math.sqrt((4 * A * D) / (B ** 2 - 4 * A * C))

  const w = Math.sqrt((4 * C * D) / (B ** 2 - 4 * A * C))

  const minX = centerX - w
  const maxX = centerX + w
  const minY = centerY - h
  const maxY = centerY + h

  return { minX, maxX, minY, maxY }
}

关于MEI相机模型图片通过图片uv坐标反推三维归一化坐标

之前写了一篇关于opencv模型下鱼眼相机的反推公式,并没有去实现opencv的普通针孔相机的畸变处理.

后来在处理MEI相机模型的时候,发现他不管是针孔还是鱼眼,处理畸变公式都是跟opencv普通针孔相机一致

那就没办法了,只能去处理他的畸变问题.但是做的时候才发现,还跟之前做的不一样,之前因为变量只有一个r,现在的变量变成了x,y两个未知数

问题转换成了二元高次方程求解未知数...

我不禁陷入了沉思...
不过好在我并不是做数学题,只是实际应用求解...
最后还是自己实现了,实现方式还是牛顿迭代法,因为畸变本身相差就不会太大,所以分别求x,y迭代的时候假设另外一个值是正确的...
最终数据x,y覆盖了-1~1的值去做测试用例,大部分用例能满足期望的精度,暂时就这样实现了...
(毕竟其他办法我也没想到/(ㄒoㄒ)/~~)
最后附上一个我提问的知乎链接,后面我自己附上了解决方案...
https://www.zhihu.com/question/597681359

关于根据鱼眼相机的uv坐标反推相机在3维场景下的入射向量

之前我写了一篇关于针孔相机和鱼眼相机从3维坐标转换为2d相机的uv坐标的公式步骤.
同样的,当我们已知图片的uv坐标时,是可以反推出在3d场景下以相机位置为起点的一条射线的(也就是得到一个向量值)
原理就是根据正向的公式进行反推,反推过程中也遇到一些坑,这里记录一下步骤和关键点
注意这里只是鱼眼相机的反推,针孔的畸变处理大同小异,实际项目里偏差很小,并未实现,将来如果实现了再补充.

  1. 根据(u,v,1)乘以相机投影矩阵的逆矩阵得到3d场景的相机坐标系下的归一化坐标(x,y,1)
  2. 根据x,y得到半径r_d=sqrt(x^2+y^2)
  3. 根据牛顿迭代法根据r_d得到实际r的值
  4. 得到r_d和r的比值scale = r_d / r
  5. 根据比值scale得到真实的(x',y')=(x/scale,y/scale)
  6. 根据真实的归一化坐标(x',y',1,1)乘以相机的平移旋转矩阵的逆矩阵得到世界坐标

这里最复杂的处理要数第3步根据r_d得到实际的r值,本质就是因为鱼眼相机导致的真实入射角偏移.
https://blog.csdn.net/qq_16137569/article/details/112398976

这篇博客的最后有提到是求thera角的一元高次方程,不过具体的实现公式并没有详细给出,还是稍微有一点坑需要注意的:
因为我们前面把相机3维相机归一化,所以这边根据半径的反正切值就是thera角度,具体公式为thera=arctan(r)
之前讲过opencv里面鱼眼相机的畸变矫正
r_d = theta(1+k1*theta^2+k2*theta^4+k3*theta^6+k4*theta^8)
所以得出r_d和r的关系是
r_d = arctan(r)+k1*arctan(r)^3+k2*arctan(r)^5+k3*arctan(r)^7+k4*arctan(r)^9
接下来要根据牛顿迭代法来求r的近似解,
牛顿迭代法公式为f(xn+1) = xn - f(xn)/f'(xn)
根据上面的公式,可以得到
f(r) = arctan(r)+k1*arctan(r)^3+k2*arctan(r)^5+k3*arctan(r)^7+k4*arctan(r)^9-r_d = 0
arctan(r)的导数为1/(1+x^2);x^n的导数为n*x^(n-1)
推出f'(r) = 1/(1+r^2)+3*k1*arctan(r)^2/(1+r^2)+5*k2*arctan(r)^4/(1+r^2)+7*k3*arctan(r)^6/(1+r^2)+9*k4*arctan(r)^8/(1+r^2)
然后第一步令 r0=r_d
计算r1=r0-f(r0)/f'(r0)
然后计算r1-r0的绝对值,如果值大于阈值(如0.000001),则计算r2=r1-f(r1)/f'(r1)
以此类推一直迭代到r(n)-r(n-1)的绝对值在阈值以下(非常接近0)或者迭代了很多次(如15次)

实现的关键代码

  const r_d = Math.sqrt(Math.pow(sx, 2) + Math.pow(sy, 2))

  const e = 1-e6
  let c = 0
  let x = r_d

  let y = getNextX(r_d, x, k1, k2, k3, k4)

  while (Math.abs(x - y) > e && c < 10) {
    x = y
    y = getNextX(r_d, x, k1, k2, k3, k4)
    c++
  }

  const r = x
  const s = r_d / r
  // 修正值
  const rx = sx / s
  const ry = sy / s



function getNextX(r_d: number, x: number, k1: number, k2: number, k3: number, k4: number) {
  const atanX = Math.atan(x)
  const derAtanX = 1 / (1 + Math.pow(x, 2)) // 1/(1+x^2)
  const fx =
    atanX + k1 * Math.pow(atanX, 3) + k2 * Math.pow(atanX, 5) + k3 * Math.pow(atanX, 7) + k4 * Math.pow(atanX, 9) - r_d

  const der_fx =
    derAtanX +
    3 * k1 * Math.pow(atanX, 2) * derAtanX +
    5 * k2 * Math.pow(atanX, 4) * derAtanX +
    7 * k3 * Math.pow(atanX, 6) * derAtanX +
    9 * k4 * Math.pow(atanX, 8) * derAtanX

  const nextX = x - fx / der_fx

  return nextX
}

去年一年有点忙,很长一段时间没有写博客,新的一年里我要立个flag给自己找点事情干干:
去年在公司的项目里我实现了一套基于2d canvas的图形框架库.自我感觉做的还可以,但是还是有不少设定上的遗憾在项目里已经不太好改,希望我能重头再写一套图形框架,对整体架构有更深的理解.拭目以待看看我能走到哪一步