基于Vue的验证码实现

一、验证码核心实现

        创建slide-verify.vue,代码如下:

<template>
  <div
    class="slide-verify"
    :style="{ width: w + 'px' }"
    id="slideVerify"
    onselectstart="return false;"
  >
    <!-- 图片加载遮蔽罩 -->
    <div :class="{ 'slider-verify-loading': loadBlock }"></div>
    <canvas :width="w" :height="h" ref="canvas"></canvas>
    <div v-if="show" @click="refresh" class="slide-verify-refresh-icon"></div>
    <canvas
      :width="w"
      :height="h"
      ref="block"
      class="slide-verify-block"
    ></canvas>
    <!-- container -->
    <div
      class="slide-verify-slider"
      :class="{
        'container-active': containerActive,
        'container-success': containerSuccess,
        'container-fail': containerFail
      }"
    >
      <div class="slide-verify-slider-mask" :style="{ width: sliderMaskWidth }">
        <!-- slider -->
        <div
          @mousedown="sliderDown"
          @touchstart="touchStartEvent"
          @touchmove="handleMoveEvent($event, 'touch')"
          @touchend="handleMoveEndEvent($event, 'touch')"
          class="slide-verify-slider-mask-item"
          :style="{ left: sliderLeft }"
        >
          <div class="slide-verify-slider-mask-item-icon"></div>
        </div>
      </div>
      <span class="slide-verify-slider-text">{{ sliderText }}</span>
    </div>
  </div>
</template>
<script>
const PI = Math.PI;

function sum(x, y) {
  return x + y;
}

function square(x) {
  return x * x;
}
export default {
  name: "SlideVerify",
  props: {
    // block length
    l: {
      type: Number,
      default: 42
    },
    // block radius
    r: {
      type: Number,
      default: 10
    },
    // canvas width
    w: {
      type: Number,
      default: 310
    },
    // canvas height
    h: {
      type: Number,
      default: 155
    },
    sliderText: {
      type: String,
      default: "Slide filled right"
    },
    accuracy: {
      type: Number,
      default: 5 // 若为 -1 则不进行机器判断
    },
    show: {
      type: Boolean,
      default: true
    },
    imgs: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      containerActive: false, // container active class
      containerSuccess: false, // container success class
      containerFail: false, // container fail class
      canvasCtx: null,
      blockCtx: null,
      block: null,
      block_x: undefined, // container random position
      block_y: undefined,
      L: this.l + this.r * 2 + 3, // block real length
      img: undefined,
      originX: undefined,
      originY: undefined,
      isMouseDown: false,
      trail: [],
      sliderLeft: 0, // block right offset
      sliderMaskWidth: 0, // mask width,
      success: false, // Bug Fixes 修复了验证成功后还能滑动
      loadBlock: true, // Features 图片加载提示,防止图片没加载完就开始验证
      timestamp: null
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      this.initDom();
      this.initImg();
      this.bindEvents();
    },
    initDom() {
      this.block = this.$refs.block;
      this.canvasCtx = this.$refs.canvas.getContext("2d");
      this.blockCtx = this.block.getContext("2d");
    },
    initImg() {
      const img = this.createImg(() => {
        // 图片加载完关闭遮蔽罩
        this.loadBlock = false;
        this.drawBlock();
        this.canvasCtx.drawImage(img, 0, 0, this.w, this.h);
        this.blockCtx.drawImage(img, 0, 0, this.w, this.h);
        let { block_x: x, block_y: y, r, L } = this;
        let _y = y - r * 2 - 1;
        let ImageData = this.blockCtx.getImageData(x, _y, L, L);
        this.block.width = L;
        this.blockCtx.putImageData(ImageData, 0, _y);
      });
      this.img = img;
    },
    drawBlock() {
      this.block_x = this.getRandomNumberByRange(
        this.L + 10,
        this.w - (this.L + 10)
      );
      this.block_y = this.getRandomNumberByRange(
        10 + this.r * 2,
        this.h - (this.L + 10)
      );
      this.draw(this.canvasCtx, this.block_x, this.block_y, "fill");
      this.draw(this.blockCtx, this.block_x, this.block_y, "clip");
    },
    draw(ctx, x, y, operation) {
      let { l, r } = this;
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
      ctx.lineTo(x + l, y);
      ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
      ctx.lineTo(x + l, y + l);
      ctx.lineTo(x, y + l);
      ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
      ctx.lineTo(x, y);
      ctx.lineWidth = 2;
      ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
      ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
      ctx.stroke();
      ctx[operation]();
      // Bug Fixes 修复了火狐和ie显示问题
      ctx.globalCompositeOperation = "destination-over";
    },
    createImg(onload) {
      const img = document.createElement("img");
      img.crossOrigin = "Anonymous";
      img.onload = onload;
      img.onerror = () => {
        img.src = this.getRandomImg();
      };
      img.src = this.getRandomImg();
      return img;
    },
    // 随机生成img src
    getRandomImg() {
      // return require('../assets/img.jpg')
      const len = this.imgs.length;
      return len > 0
        ? this.imgs[this.getRandomNumberByRange(0, len - 1)]
        : // "https://bing.ioliu.cn/v1/rand?w=300&h=150";
          "https://source.unsplash.com/300x150/?book,library";
      // "https://api.dujin.org/pic/fengjing";
    },
    getRandomNumberByRange(start, end) {
      return Math.round(Math.random() * (end - start) + start);
    },
    refresh() {
      this.reset();
      this.$emit("refresh");
    },
    sliderDown(event) {
      if (this.success) return;
      this.originX = event.clientX;
      this.originY = event.clientY;
      this.isMouseDown = true;
      this.timestamp = +new Date();
    },
    touchStartEvent(e) {
      if (this.success) return;
      e.preventDefault();
      this.originX = e.changedTouches[0].pageX;
      this.originY = e.changedTouches[0].pageY;
      this.isMouseDown = true;
      this.timestamp = +new Date();
    },
    bindEvents() {
      document.addEventListener("mousemove", this.handleMoveEvent);
      document.addEventListener("mouseup", this.handleMoveEndEvent);
    },
    // 处理函数抽离
    handleMoveEvent: throttle(function(e, type = "mouse") {
      if (!this.isMouseDown) return false;
      const moveX =
        type === "mouse"
          ? e.clientX - this.originX
          : e.changedTouches[0].pageX - this.originX;
      const moveY =
        type === "mouse"
          ? e.clientY - this.originY
          : e.changedTouches[0].pageY - this.originY;
      if (moveX < 0 || moveX + 38 >= this.w) return false;
      this.sliderLeft = moveX + "px";
      let blockLeft = ((this.w - 40 - 20) / (this.w - 40)) * moveX;
      this.block.style.left = blockLeft + "px";

      this.containerActive = true; // add active
      this.sliderMaskWidth = moveX + "px";
      this.trail.push(moveY);
    }),
    handleMoveEndEvent(e, type = "mouse") {
      if (!this.isMouseDown) return false;
      this.isMouseDown = false;
      if (
        (type === "mouse" && e.clientX === this.originX) ||
        (type === "touch" && e.changedTouches[0].pageX === this.originX)
      )
        return false;
      this.containerActive = false; // remove active
      this.timestamp = +new Date() - this.timestamp;

      const { spliced, TuringTest } = this.verify();
      if (spliced) {
        if (this.accuracy === -1) {
          this.containerSuccess = true;
          this.success = true;
          this.$emit("success", this.timestamp);
          return;
        }
        if (TuringTest) {
          // succ
          this.containerSuccess = true;
          this.success = true;
          this.$emit("success", this.timestamp);
        } else {
          this.containerFail = true;
          this.$emit("again");
        }
      } else {
        this.containerFail = true;
        this.$emit("fail");
        setTimeout(() => {
          this.reset();
        }, 1000);
      }
    },
    verify() {
      const arr = this.trail; // drag y move distance
      const average = arr.reduce(sum) / arr.length; // average
      const deviations = arr.map(x => x - average); // deviation array
      const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); // standard deviation
      const left = parseInt(this.block.style.left);
      const accuracy =
        this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy;
      return {
        spliced: Math.abs(left - this.block_x) <= accuracy,
        TuringTest: average !== stddev // equal => not person operate
      };
    },
    reset() {
      this.success = false;
      this.containerActive = false;
      this.containerSuccess = false;
      this.containerFail = false;
      this.sliderLeft = 0;
      this.block.style.left = 0;
      this.sliderMaskWidth = 0;
      // canvas
      let { w, h } = this;
      this.canvasCtx.clearRect(0, 0, w, h);
      this.blockCtx.clearRect(0, 0, w, h);
      this.block.width = w;

      // generate img
      this.img.src = this.getRandomImg();
      this.$emit("fulfilled");
    }
  },
  destroyed() {
    document.removeEventListener("mousemove", this.handleMoveEvent);
    document.removeEventListener("mouseup", this.handleMoveEndEvent);
  }
};

function throttle(
  fn,
  interval = 50,
  options = { leading: true, trailing: true }
) {
  const { leading, trailing, resultCallback } = options;
  let lastTime = 0;
  let timer = null;

  const _throttle = function(...args) {
    return new Promise((resolve, reject) => {
      const nowTime = new Date().getTime();
      if (!lastTime && !leading) lastTime = nowTime;

      const remainTime = interval - (nowTime - lastTime);
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer);
          timer = null;
        }

        const result = fn.apply(this, args);
        if (resultCallback) resultCallback(result);
        resolve(result);
        lastTime = nowTime;
        return;
      }

      if (trailing && !timer) {
        timer = setTimeout(() => {
          timer = null;
          lastTime = !leading ? 0 : new Date().getTime();
          const result = fn.apply(this, args);
          if (resultCallback) resultCallback(result);
          resolve(result);
        }, remainTime);
      }
    });
  };

  _throttle.cancel = function() {
    if (timer) clearTimeout(timer);
    timer = null;
    lastTime = 0;
  };

  return _throttle;
}
</script>
<style scoped>
.slide-verify {
  position: relative;
}

/* 图片加载样式 */
.slider-verify-loading {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  background: rgba(255, 255, 255, 0.9);
  z-index: 999;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% {
    opacity: 0.7;
  }
  100% {
    opacity: 9;
  }
}

.slide-verify-block {
  position: absolute;
  left: 0;
  top: 0;
}

.slide-verify-refresh-icon {
  position: absolute;
  right: 0;
  top: 0;
  width: 34px;
  height: 34px;
  cursor: pointer;
  background: url("../assets/icon_light.png") 0 -437px;
  background-size: 34px 471px;
}

.slide-verify-slider {
  position: relative;
  text-align: center;
  width: 100%;
  height: 40px;
  line-height: 40px;
  margin-top: 15px;
  background: #f7f9fa;
  color: #45494c;
  border: 1px solid #e4e7eb;
}

.slide-verify-slider-mask {
  position: absolute;
  left: 0;
  top: 0;
  height: 40px;
  border: 0 solid #1991fa;
  background: #d1e9fe;
}

.slide-verify-slider-mask-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 40px;
  height: 40px;
  background: #fff;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
  cursor: pointer;
  transition: background 0.2s linear;
}

.slide-verify-slider-mask-item:hover {
  background: #1991fa;
}

.slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {
  background-position: 0 -13px;
}

.slide-verify-slider-mask-item-icon {
  position: absolute;
  top: 15px;
  left: 13px;
  width: 14px;
  height: 12px;
  background: url("../assets/icon_light.png") 0 -26px;
  background-size: 34px 471px;
}
.container-active .slide-verify-slider-mask-item {
  height: 38px;
  top: -1px;
  border: 1px solid #1991fa;
}

.container-active .slide-verify-slider-mask {
  height: 38px;
  border-width: 1px;
}

.container-success .slide-verify-slider-mask-item {
  height: 38px;
  top: -1px;
  border: 1px solid #52ccba;
  background-color: #52ccba !important;
}

.container-success .slide-verify-slider-mask {
  height: 38px;
  border: 1px solid #52ccba;
  background-color: #d2f4ef;
}

.container-success .slide-verify-slider-mask-item-icon {
  background-position: 0 0 !important;
}

.container-fail .slide-verify-slider-mask-item {
  height: 38px;
  top: -1px;
  border: 1px solid #f57a7a;
  background-color: #f57a7a !important;
}

.container-fail .slide-verify-slider-mask {
  height: 38px;
  border: 1px solid #f57a7a;
  background-color: #fce1e1;
}

.container-fail .slide-verify-slider-mask-item-icon {
  top: 14px;
  background-position: 0 -82px !important;
}

.container-active .slide-verify-slider-text,
.container-success .slide-verify-slider-text,
.container-fail .slide-verify-slider-text {
  display: none;
}
</style>

        基于slide-verify.vue创建组件,index.js代码如下:

import SlideVerify from './slide-verify.vue'

const plugins = {
  install(Vue) {
      Vue.component(SlideVerify.name, SlideVerify)
  }
}

if (typeof window !== 'undefined' && window.Vue) {
    window.Vue.use(SlideVerify)
}

export default plugins

二、在项目中使用验证码

        在main.js中应用:

import { createApp } from 'vue'
import App from './App.vue'
// 导入验证码
import SlideVerify from './xxx/index'

const vue = createApp(App)
vue.use(SlideVerify) // 在vue中使用
vue.mount('#app')

        准备验证码图片:

         其中icon_light.png图标图片如下,其他img.jpg图片可以是任意图片。

        在vue中使用验证码,App.vue代码如下:

<template>
  <div id="app">
    <slide-verify ref="slideBlock" @success="onSuccess" @again="onAgain" @fulfilled="onFulfilled" @fail="onFail"
                  @refresh="onRefresh" :slider-text="text" :imgs="imgs" :accuracy="accuracy"></slide-verify>
    <div>{{ msg }}</div>
    <button class="btn" @click="handleClick">在父组件点击刷新</button>
  </div>
</template>

<script setup>
import img0 from './assets/img.jpg';
import img1 from './assets/img1.jpg';
import img2 from './assets/img2.jpg';
import img3 from './assets/img3.jpg';
import img4 from './assets/img4.jpg';
import img5 from './assets/img5.jpg';
import {ref} from "vue";

const slideBlock = ref();
const msg = ref('');
const text = ref('向右滑动->');
const imgs = ref([
  img0,
  img1,
  img2,
  img3,
  img4,
  img5,
]);
const accuracy = ref(1); // 精确度小,可允许的误差范围小;为1时,则表示滑块要与凹槽完全重叠,才能验证成功。默认值为5


const onSuccess = (times) => {
  console.log('验证通过');
  msg.value = `login success, 耗时${(times / 1000).toFixed(1)}s`;
};
const onFail = () => {
  console.log('验证不通过');
  msg.value = ''
};
const onRefresh = () => {
  console.log('点击了刷新小图标');
  msg.value = ''
};
const onFulfilled = () => {
  console.log('刷新成功啦!');
};
const onAgain = () => {
  console.log('检测到非人为操作的哦!');
  msg.value = 'try again';
  // 刷新
  handleClick();
};
const handleClick = () => {
  slideBlock.value.reset();
  msg.value = ''
}

</script>

<style scoped>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-top: 60px;
}

.btn {
  margin-top: 20px;
  outline: 0;
  border: none;
  padding: 8px 15px;
  border-radius: 5px;
  color: #fff;
  background-color: #1890ff;
  cursor: pointer;
}

.btn:active {
  box-shadow: 1px 5px 0 rgba(0, 0, 0, 0.1) inset;
}
</style>

        运行项目查看:

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

全面了解CC攻击和防范策略

前言 “ CC攻击的原理就是攻击者控制某些主机不停地发大量数据包给对方服务器造成服务器资源耗尽&#xff0c;一直到宕机崩溃。” 什么是CC攻击&#xff1f; CC攻击前身是一个名为Fatboy的攻击程序&#xff0c;而之所以后来人们会称之为CC&#xff0c;也叫HTTP-FLOOD&#xff…

modbus开源库libmodbus的C语言使用记录(实现简单的modbus主机/丛机程序,解决libmodbus库安装出现的问题)

libmodbus简介 libmodbus 是一个开源的、跨平台的C库,用于实现Modbus通讯协议。它支持Modbus RTU(RS-232/485)和Modbus TCP协议,可以使开发者方便地在项目中集成Modbus通讯功能。libmodbus的设计目标是简单、灵活和高效,适用于各种大小的嵌入式和桌面应用。 编译运行测试…

Orca: FSS-based Secure Training and Inference with GPUs

目录 1.Abstract2.Accelerating FSS on a GPU2.1 Accelerating FSS-based compute on GPU2.1.1 Faster AES computation (AES)2.1.2 Optimized data layout for cache locality (LAYOUT)2.1.3 Optimizing memory footprint (MEM) 2.2 Reducing time to read FSS keys2.2.1 Bypa…

计算机图形学入门games103——碰撞检测

一.怎么发现碰撞&#xff0c;有没有碰撞 碰撞最难的是布料的碰撞 碰撞检测分为两个部分&#xff1a; 第一阶段&#xff1a;去除不可能发生碰撞的元素&#xff0c;输出的就是可能碰撞的备选目标&#xff0c;这阶段叫做碰撞剔除 这常用的套路有两种&#xff1a;第一种是Spati…

算法题解记录26+++翻转二叉树(百日筑基)

题目描述&#xff1a; 题目难度&#xff1a;简单 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1]示例 2&#xff1a; 输入&#xff1a;roo…

C++ TCP发送Socket数据

DEVC需要加入ws2_32库 #include <iostream> #include <winsock2.h>#pragma comment(lib, "ws2_32.lib")void sendData(const char* ip, int port, const char* data) {WSADATA wsaData;SOCKET sockfd;struct sockaddr_in server_addr;// 初始化Winsock…

HDR视频相关标准-HDR vivid(二)

上文介绍了HDRvivid的一些技术。今天从全局角度来看看HDR视频的处理流程&#xff0c;HDR视频系统&#xff0c;即建立一个比SDR视频更大的色彩/亮度坐标体系&#xff0c;并改变系统的传输函数&#xff0c;以再现更大的色域(WCG)和更高的亮度动态范围。 菁彩 HDR技术的专业术语 …

(全面)Nginx格式化插件,Nginx生产工具,Nginx常用命令

目录 &#x1f3ab; 前言 &#x1f389; 开篇福利 &#x1f381; 开篇福利 x2 Double happiness # 介绍 # 地址 # 下载 &#x1f4bb; 命令及解析 # 整个文件系统中搜索名为nginx.conf的文件 # 编辑nginx.conf文件 # 重新加载配置文件 # 快速查找nginx.conf文件并使…

python的协程异步

参考资料 https://blog.csdn.net/qq_43380180/article/details/111573642?spm1001.2014.3001.5506 协程的概念 指的是在一个线程中&#xff0c;可以在某个地方挂起的特殊函数&#xff0c;并且可以重新在挂起处继续运行。协程不是进程&#xff0c;也不是线程。 进程 VS 线程…

Polar 上传

Polar 上传 开题&#xff0c;是一个文件上传界面 对文件后缀有过滤 测试了一下是黑名单&#xff0c;过滤了php相关的文件&#xff0c;但是没过滤.ini、.htaccess后缀的文件 对内容的过滤是<?、file&#xff0c;所以不能用.user.ini配置文件绕过 我们选择使用.htaccess配置…

晶体振荡器

一、晶振与晶体区别 晶振是有源晶振的简称&#xff0c;又叫振荡器&#xff0c;英文名称是oscillator&#xff0c;内部有时钟电路&#xff0c;只需供电便可产生振荡信号&#xff1b;晶体是无源晶振的简称&#xff0c;也叫谐振器&#xff0c;英文名称是crystal&#xff0c;是无极…

第197题|奇偶性的四则运算,你掌握了吗?|函数强化训练(四)|武忠祥老师每日一题 5月22日

解题思路&#xff1a;这道题如果我们会21号的题的话&#xff0c;简直是小菜一碟&#xff01;主要就是要用到下面这个结论&#xff1a; &#xff08;A&#xff09; 直接看奇偶性我们不好看&#xff0c;我们需要拆项&#xff1a; 我们先看前一项的奇偶性&#xff0c;x是奇函数&a…

民国漫画杂志《时代漫画》第13期.PDF

时代漫画13.PDF: https://url03.ctfile.com/f/1779803-1247458360-14efab?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps:资源来源网络&#xff01;

P1【知识点】【数据结构】【链表LinkedList】C++版

链表是一种逻辑上连续&#xff0c;内存上分散的线性表数据结构&#xff0c;是用一组任意的空间&#xff08;可以连续&#xff0c;也可以不连续&#xff09;来存放数据元素。每个数据元素成为一个”结点“&#xff0c;每个结点由数据域和指针域组成。 访问元素&#xff08;Acce…

特征融合篇 | YOLOv8改进之引入轻量级跨尺度特征融合模块CCFM | 源自RT-DETR

前言:Hello大家好,我是小哥谈。CCFM(Cross-Scale Feature Fusion Module)即为跨尺度特征融合模块。这个模块的作用是将不同尺度的特征通过融合操作整合起来,以增强模型对于尺度变化的适应性和对小尺度对象的检测能力。CCFM可以有效地整合细节特征和上下文信息,从而提高模…

【全开源】简单商城系统(PC/UniAPP)

轻松构建您的在线商店 在当今数字化时代&#xff0c;拥有一个在线商店对于许多商家来说已成为必不可少的营销手段。为了满足这一需求&#xff0c;我们推出了“简单商城系统源码”&#xff0c;让您轻松构建并管理您的在线商店。 一、简单易用&#xff0c;快速上手 “简单商城…

python demo

文章背景&#xff0c;记录python 小demo 集合 1、使用python matplotlib库描绘曲线 import matplotlib.pyplot as NLAx_index [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100] y_index [6.1, 7.4, 9.1, 11.1, 12.69, 14.35, 16.1, 1…

成都爱尔周进院长提醒当双眼度数差距过大,我们该做些什么

每个人的用眼方式、用眼习惯且两只眼睛“天生条件”不一定相同&#xff0c;当发生近视&#xff0c;双眼近视程度也就可能不同&#xff0c;双眼度数必然会变得不一样。当双眼度数产生差异&#xff0c;尤其是当双眼度数差别过大时会引发哪些问题&#xff1f; 双眼度数不一致&…

面试八股之MySQL篇4——事务篇

&#x1f308;hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;一名不断学习的码农&#xff0c;很高兴你能来阅读。 ✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。 &#x1f3c3;人生之义&#xff0c;在于追求&#xff0c;不在成败&#xff0c;勤通…

【ARFoundation自学03】AR Point Cloud 点云(参考点标记)功能详解

和平面识别框架一样 1为XR Origin添加AR Point Cloud Manager组件 然后你的ar应用就具备了点云识别功能&#xff0c;就这么简单 2.可视化这些云点 创建一个美术效果的预制体&#xff0c;人家提供了预设模板 然后拖到仓库&#xff08;ASSETS&#xff09;创建预制体&#xff…