分类 默认分类 下的文章

Threejs实现Points对象实时指向指定坐标

全景图里常常会往场景里添加箭头,希望箭头可以指向某个3d坐标
箭头指向目标点

matrial关键代码:

function createMaterial(size = 24) {
  const vertexShader = `
    attribute vec3 a_direction;
    uniform float u_size;
    uniform float u_aspect;
    varying float v_rotation;

    void main(){
      gl_PointSize = u_size;
      // 箭头的位置坐标
      vec4 mvPosition = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      // 箭头指向的位置坐标
      vec4 targetPos = projectionMatrix * modelViewMatrix * vec4(a_direction, 1.0);

      vec2 dir = targetPos.xy/targetPos.w -mvPosition.xy/mvPosition.w;
      float theta = atan(dir.y / u_aspect, dir.x);

      v_rotation =  theta; //  mod( theta + PI, 2.0 * PI );
      gl_Position = mvPosition;
    }
  `;
  const fragmentShader = `
    uniform vec3 u_color;
    uniform float u_opacity;
    uniform sampler2D u_map;
    varying float v_rotation;

    void main(){

      vec2 uv = gl_PointCoord - vec2(0.5);

      float xn = uv.x * cos(v_rotation) - uv.y * sin(v_rotation);
      float yn = uv.x * sin(v_rotation) + uv.y * cos(v_rotation);

      vec2 new_uv = vec2(xn +0.5, yn+0.5);

      if(new_uv.x < 0.0 || new_uv.x > 1.0 || new_uv.y < 0.0 || new_uv.y > 1.0){
        discard;
      }

      float mapColor = texture2D(u_map, new_uv).r;

      gl_FragColor = vec4(u_color, u_opacity * mapColor);
    }
  `;
  const texture = createCanvasTexture();
  const usize = size * window.devicePixelRatio;
  const material = new ShaderMaterial({
    uniforms: {
      u_size: { value: usize },
      u_color: { value: new Color(0xff0000) },
      u_opacity: { value: 1 },
      u_map: { value: texture },
      u_aspect: { value: aspect },
    },
    defines: {
      PI: Math.PI,
    },
    vertexShader,
    fragmentShader,
    transparent: true,
  });
  return material;
}

需要注意的是shader里面获取到的向量还需要除以canvas真实宽高比,否则计算出来的角度会不对.

附上github源码及demo

github源码地址
示例demo

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