用简单例子讲清楚webgl模板测试
文章目录
- 搭建简易的webgl环境
- 绘制简单三角形(不带stencilTest)
- 绘制另一个三角形(不带模板测试)
- 加入模板测试
- 总结
- 调参练习
搭建简易的webgl环境
一直以来,我只是想通过搭建纯webgl环境,进行开发,来清楚地了解基础webgl功能,如果在C++环境里面使用opengl,还需要配置加载各种 lib,以及编译,容易错,而且里面有很多是其他各种库里面的函数,不能最大限度地接近原理。偶然间,发现了一个很简单的方法,基于最基本的webgl函数:WebGLRenderingContext里面的api。下面推荐一下,可以方便大家一起学习。
进入mdn在线文档:
https://developer.mozilla.org/zh-CN/
这里面不仅有详细地说明,还有一个平台给我们实践各种代码,如下图所示,点击其中的play:

则进入写代码平台:
https://developer.mozilla.org/zh-CN/play
平台就这么搭建好了,不用写任何东西,也不用安装任何软件,编译任何代码。否则,很多情况下,我的耐心都在各种无关的操作中被消磨掉了,到头来什么也没学到。
绘制简单三角形(不带stencilTest)
把下面这段js代码复制到上面写代码平台的最下面js框里面,然后右边会出现一个三角形:
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
document.body.append(canvas); // 创建和将 canvas 加入页面
const gl = canvas.getContext("webgl");
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 创建一个顶点着色器
gl.shaderSource(vertexShader,`attribute vec4 a_position;void main() {gl_Position = a_position; // 设置顶点位置}
`,
); // 编写顶点着色器代码
gl.compileShader(vertexShader); // 编译着色器代码const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 创建一个片元着色器
gl.shaderSource(fragmentShader,`precision mediump float;uniform vec4 u_color;void main() {gl_FragColor = u_color; // 设置片元颜色}
`,
); // 编写片元着色器代码
gl.compileShader(fragmentShader); // 编译着色器代码const program = gl.createProgram(); // 创建一个程序
gl.attachShader(program, vertexShader); // 添加顶点着色器
gl.attachShader(program, fragmentShader); // 添加片元着色器
gl.linkProgram(program); // 连接 program 中的着色器gl.useProgram(program); // 告诉 webgl 用这个 program 进行渲染const colorLocation = gl.getUniformLocation(program, "u_color");
// 获取 u_color 变量位置
gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1); // 设置它的值const positionLocation = gl.getAttribLocation(program, "a_position");
// 获取 a_position 位置
const positionBuffer = gl.createBuffer();
// 创建一个顶点缓冲对象,返回其 ID,用来放三角形顶点数据,
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将这个顶点缓冲对象绑定到 gl.ARRAY_BUFFER
// 后续对 gl.ARRAY_BUFFER 的操作都会映射到这个缓存
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([0, 0.5, 0.5, 0, -0.5, -0.5]), // 三角形的三个顶点// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
// 将顶点数据加入的刚刚创建的缓存对象gl.vertexAttribPointer(// 告诉 OpenGL 如何从 Buffer 中获取数据positionLocation, // 顶点属性的索引2, // 组成数量,必须是1,2,3或4。我们只提供了 x 和 ygl.FLOAT, // 每个元素的数据类型false, // 是否归一化到特定的范围,对 FLOAT 类型数据设置无效0, // stride 步长 数组中一行长度,0 表示数据是紧密的没有空隙,让OpenGL决定具体步长0, // offset 字节偏移量,必须是类型的字节长度的倍数。
);
gl.enableVertexAttribArray(positionLocation);
// 开启 attribute 变量额,使顶点着色器能够访问缓冲区数据gl.clearColor(0, 1, 1, 1); // 设置清空颜色缓冲时的颜色值
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区,也就是清空画布gl.drawArrays(// 从数组中绘制图元gl.TRIANGLES, // 画三角形0, // 从哪个点开始画3, // 需要用到多少个点
);

上面是绘制简单三角形的webgl js代码,为了便于理解,这篇文章从这个最一般的helloworld讲起,通过加入一些变化,说明stenciltest的原理,因为我在网上看到很多有各种复杂的部分,什么贴纹理啊,绘制地板,box啊,甚至还加了动画,实在不方便对单个模板测试功能的理解。
绘制另一个三角形(不带模板测试)
把以下这一小段代码加入到刚才的js代码后面,可以绘制两个重叠的三角形:
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1.0, 0.0, 0.0, -1.0, 1., 0.0]), // 三角形的三个顶点// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
gl.drawArrays(// 从数组中绘制图元gl.TRIANGLES, // 画三角形0, // 从哪个点开始画3, // 需要用到多少个点
);

加入模板测试
在刚才的基础上做如下修改:
- 修改初始化gl的代码 ,原来的
const gl = canvas.getContext("webgl");改成:const gl = canvas.getContext("webgl",{stencil: true}); - 在绘制第一个三角形之前加入如下代码:
gl.enable(gl.STENCIL_TEST);
gl.colorMask(false, false, false, false);
gl.stencilFunc(gl.ALWAYS, 1, 255);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
其中第一句是开启模板测试,第二句设置color遮罩,目的是不让它真正显示在结果框中,但是还是影响了模板测试,并且可以修改了stencilbuffer里面的值,第三句是设置通过模板测试的条件,gl.ALWAYS也就是永远通过,1代表ref(参照值),255代表位掩码,这里没用,因为是always通过模板测试,第四句是设置模板测试不通过或者通过而深度测试不通过或者模板测试和深度测试都通过以后干什么,这里都设置为REPLACE,表示不管咋样,要把模板buffer里面的值全部替换成ref(参照值),第三句已设定参照值为1。这些都要在绘制第一个三角形时生效,还记得第一个三角形是什么吗?那是一个倾斜的三角形,可以回头看截图。
这样设置后在绘制完第一个三角形以后stencilbuffer里面是这样子的(示意图):

其中只有在中间三角形地方的片源都是1,其他都是0,因为模板测试缓冲区默认是0,其他没绘制的地方保持不变,绘制地方(也就是三角形区域)不论是深度测试和模板测试是否通过都用ref值1替换。
这样设置以后整体运行后是看不到任何三角形的,因为gl.colorMask(false, false, false, false)这句话的原因。
- 在绘制第二个三角形之前加入以下代码:
gl.colorMask(true, true, true, true);
gl.stencilFunc(gl.NOTEQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
第一句是设置color遮罩,这里红绿蓝透明都是true,意思是都可以绘制出来了,所以第二个三角形就能看到了,下面一句话是设置模板测试通过的条件,不等于1,具体是:ref=1, mask=255,令:逐片源看stenbuffer里面当前存储值=buf,设置为NOTEQUAL则意思是:ref&mask!==buf&mask,翻译过来就是:ref按位与mask的值不等于逐片源stencilbuffer里面当前值按位与mask的值,其中ref按位与mask为1,恒定,逐片源stencilbuffer里面当前值按位与mask则是:非第一个三角形绘制区域为0,第一个三角形绘制区域为1,所以这里模板测试通过的条件就是:非第一个三角形绘制区域,将通过模板测试。
而第三句规定了不通过模板测试和通过模板测试以及通过模板测试和深度测试之后都不改变stencilbuffer的当前值。
在以上设置完成后绘制的第二个三角形如下所示:

从图中看,第二个三角形中在第一个三角形区域的片源被去掉了,因为那些片源没有通过模板测试。符合我们之前的设置结果。
修改后的整体js代码如下:
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
document.body.append(canvas); // 创建和将 canvas 加入页面
const gl = canvas.getContext("webgl",{stencil:true});
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 创建一个顶点着色器
gl.shaderSource(vertexShader,`attribute vec4 a_position;void main() {gl_Position = a_position; // 设置顶点位置}
`,
); // 编写顶点着色器代码
gl.compileShader(vertexShader); // 编译着色器代码const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 创建一个片元着色器
gl.shaderSource(fragmentShader,`precision mediump float;uniform vec4 u_color;void main() {gl_FragColor = u_color; // 设置片元颜色}
`,
); // 编写片元着色器代码
gl.compileShader(fragmentShader); // 编译着色器代码const program = gl.createProgram(); // 创建一个程序
gl.attachShader(program, vertexShader); // 添加顶点着色器
gl.attachShader(program, fragmentShader); // 添加片元着色器
gl.linkProgram(program); // 连接 program 中的着色器gl.useProgram(program); // 告诉 webgl 用这个 program 进行渲染const colorLocation = gl.getUniformLocation(program, "u_color");
// 获取 u_color 变量位置
gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1); // 设置它的值const positionLocation = gl.getAttribLocation(program, "a_position");
// 获取 a_position 位置
const positionBuffer = gl.createBuffer();
// 创建一个顶点缓冲对象,返回其 ID,用来放三角形顶点数据,
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将这个顶点缓冲对象绑定到 gl.ARRAY_BUFFER
// 后续对 gl.ARRAY_BUFFER 的操作都会映射到这个缓存
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([0, 0.5, 0.5, 0, -0.5, -0.5]), // 三角形的三个顶点// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
// 将顶点数据加入的刚刚创建的缓存对象gl.vertexAttribPointer(// 告诉 OpenGL 如何从 Buffer 中获取数据positionLocation, // 顶点属性的索引2, // 组成数量,必须是1,2,3或4。我们只提供了 x 和 ygl.FLOAT, // 每个元素的数据类型false, // 是否归一化到特定的范围,对 FLOAT 类型数据设置无效0, // stride 步长 数组中一行长度,0 表示数据是紧密的没有空隙,让OpenGL决定具体步长0, // offset 字节偏移量,必须是类型的字节长度的倍数。
);
gl.enableVertexAttribArray(positionLocation);
// 开启 attribute 变量额,使顶点着色器能够访问缓冲区数据gl.clearColor(0, 1, 1, 1); // 设置清空颜色缓冲时的颜色值
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区,也就是清空画布
gl.enable(gl.STENCIL_TEST);
gl.colorMask(false, false, false, false);
gl.stencilFunc(gl.ALWAYS, 1, 255);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);gl.drawArrays(// 从数组中绘制图元gl.TRIANGLES, // 画三角形0, // 从哪个点开始画3, // 需要用到多少个点
);
gl.colorMask(true, true, true, true);
gl.stencilFunc(gl.NOTEQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1.0, 0.0, 0.0, -1.0, 1., 0.0]), // 三角形的三个顶点// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
gl.drawArrays(// 从数组中绘制图元gl.TRIANGLES, // 画三角形0, // 从哪个点开始画3, // 需要用到多少个点
);
总结
通过实践,我发现模板测试是这样的一个思路,总结如下:
在各种绘制语句之前加入一些配置项,这些配置项的目的是修改stencilbuffer以及决定这次的绘制是不是能通过模板测试,因为只有通过模板测试和深度测试的片源才能最终被绘制出来,而修改后的stencilbuffer里面的值会影响下次绘制是否能通过模板测试,所以模板测试并不会修改绘制出的东西的颜色啥的,他是一种高级裁剪。通过这种方式,经过多次不清空stencilbuffer的绘制过程,在stencilbuffer里面形成一幅独一无二的图像,这种图像是前面多次绘制的结果(它可能是复杂的),不是简单的行列排布,而是依赖于之前图像的一些特征,不断地影响着后面的绘制结果。
调参练习
为了便于理解我们可以修改其中的某些参数,来测试自己是不是真正理解,比如,我们把第二次绘制之前的模板测试条件设置语句中notequal改成equal:gl.stencilFunc(gl.EQUAL, 1, 255);那么我们将得到另一个三角形:

最后留一道思考题,如何修改以上代码,绘制出以下图形呢?

快来尝试一下吧。
最近在学习英语,所以想用英语表达一下一点学习感悟:
Many engineers want to create the complex system which looks like cool and amazing at the beginning and ignore the easy and basic things, I know this feeling and experienced that, it is about anxiety, about unexpectable future, which tells me that we must work hard. After years of working, I found it is impossible, every skill need to practice from the easy, little, looks boring system, it may not be needed by our boss, by the society, we can not get any afford from that, but we need to face that, if we just copy any other code from the internet, and make out something that looks well, it doesn’t mean I have mastered that skill. Perhaps, it listened stupid, what? to make wheels by ourselves? What time is now? yeah, I confess it is stupid, but I just want to be the person, only in my way to understand the world.
相关文章:
用简单例子讲清楚webgl模板测试
文章目录 搭建简易的webgl环境绘制简单三角形(不带stencilTest)绘制另一个三角形(不带模板测试)加入模板测试总结调参练习 搭建简易的webgl环境 一直以来,我只是想通过搭建纯webgl环境,进行开发,来清楚地了…...
区块链(8):p2p去中心化之websoket服务端实现业务逻辑
1 业务逻辑 例如 peer1和peer2之间相互通信 peer1通过onopen{ write(Mesage(QUERY_LATEST))} 向peer2发送消息“我要最新的区块”。 peer2通过onMessage收到消息,通过handleMessage方法对消息进行处理。 handleMessage根据消息类型进行处理 RESPONSE_BLOCKCHAIN:返回区块链…...
composer安装与设置
1、到官网下载 composer.phar。下载地址:Composer 2、将下载的composer.phar 复制到 composer 文件夹中 3、在composer文件夹中新建文件 composer.bat,内容为 php "%~dp0composer.phar" %* 5、设置环境变量的path,添加composer文件夹...
unordered_map/unordered_set的学习[unordered系列]
文章目录 1.老生常谈_遍历2.性能测试3.OJ训练3.1存在重复元素3.2两个数组的交集Ⅱ3.3两句话中的不常见单词3.4两个数组的交集3.5在长度2N的数组中找出重复N次的元素 1.老生常谈_遍历 #pragma once #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <l…...
C++位图—布隆过滤器
目录 位图概念位图应用 布隆过滤器简介布隆过滤器的优缺点布隆过滤器应用场景布隆过滤器实现布隆过滤器误判率分析 总结 位图概念 位图是一种数据结构,用于表示一组元素的存在或不存在,通常用于大规模数据集的快速查询。它基于一个位数组(或位…...
SQL SELECT 语句进阶
之前探讨了SQL SELECT 语句的基础内容,包括语法、字段选择、记录限制和数据源指定。今天将进一步深入,探讨多表连接、过滤结果集和逻辑运算等高级主题,还有LIKE 模糊查询、ORDER BY 对结果集排序、运用聚合函数汇总结果以及 GROUP BY 子句与相关应用。 本文将继续使用《三国…...
Mac程序坞美化工具 uBar
uBar是一款为Mac用户设计的任务栏增强软件,它可以为您提供更高效和更个性化的任务管理体验。 以下是uBar的一些主要特点和功能: 更直观的任务管理:uBar改变了Mac上传统的任务栏设计,将所有打开的应用程序以类似于Windows任务栏的方…...
【数据结构】排序之插入排序和选择排序
🔥博客主页:小王又困了 📚系列专栏:数据结构 🌟人之为学,不日近则日退 ❤️感谢大家点赞👍收藏⭐评论✍️ 目录 一、排序的概念及其分类 📒1.1排序的概念 📒1.2排序…...
6.html表单
HTML表单(HTML form)是网页中用于收集用户输入数据的一种方式。表单由多个表单元素组成,通常包括输入框,复选框,单选按钮,下拉列表和提交按钮等。 HTML表单元素的基本结构如下: <form acti…...
【python学习第11节:numpy】
文章目录 一,numpy(上)1.1基础概念1.2数组的属性1.3数组创建1.4 类型转换1.5ndarry基础运算(上)矢量化运算1.6拷贝和视图1.6.1完全不复制1.6.2视图或浅拷贝1.6.3深拷贝 1.7索引,切片和迭代1.7.1一维数组1.7…...
Eclipse 主网即将上线迎空投预期,Zepoch 节点或成受益者?
目前,Zepoch 节点空投页面中,模块化 Layer2 Rollup 项目 Eclipse 出现在其空投列表中。 配合近期 Eclipse 宣布了其将由 SVM 提供支持的 Layer2 主网架构,并将在今年年底上线主网的消息后,不免引发两点猜测:一个是 Ecl…...
JavaSE | 初识Java(四) | 输入输出
基本语法 System.out.println(msg); // 输出一个字符串, 带换行 System.out.print(msg); // 输出一个字符串, 不带换行 System.out.printf(format, msg); // 格式化输出 println 输出的内容自带 \n, print 不带 \n printf 的格式化输出方式和 C 语言的 printf 是基本一致的 代码…...
车牌超分辨率:License Plate Super-Resolution Using Diffusion Models
论文作者:Sawsan AlHalawani,Bilel Benjdira,Adel Ammar,Anis Koubaa,Anas M. Ali 作者单位:Prince Sultan University 论文链接:http://arxiv.org/abs/2309.12506v1 内容简介: 1)方向:图像超分辨率技术…...
如何制作在线流程图?6款在线工具帮你轻松搞定
流程图,顾名思义 —— 用视觉化的方式来描述一种过程或流程。它可以应用于各种领域,从业务流程,算法,到计算机程序等。然而,在创建流程图时,可能会遇到许多问题或者困惑,如缺乏专业的设计技能&a…...
反SSDTHOOK的另一种思路-0环实现自己的系统调用
反SSDTHOOK的另一种思路-0环实现自己的系统调用 大家都知道我们在应用层使用系统api除了gdi相关的都会走中断门或者systementer进0环然后在走ssdt表去执行0环的函数 这也就导致了ssdthook可以挡下大部分的api调用,那如果我们进0环走另外一条路线的话不通过ssdt就可…...
Certbot签发和续费泛域名SSL证书(通过DNS TXT记录来验证域名有效性)
我们在使用let’s encrypt获取免费的HTTPS证书的时候,let’s encrypt需要对域名进行验证,以确保域名是你自己的 之前用默认的文件验证方式总有奇怪的问题导致失败,我也是很无奈,于是改用验证DNS-TXT记录的方式来验证,而…...
PY32F003F18之RTC
一、RTC振荡器 PY32F003F18实时时钟的振荡器是内部RC振荡器,频率为32.768KHz。它也可以使用HSE时钟,不建议使用。HAL库提到LSE振荡器,但PY32F003F18实际上没有这个振荡器。 缺点:CPU掉电后,需要重新配置RTCÿ…...
redis主从从,redis-7.0.13
redis主从从,redis-7.0.13 下载redis安装redis安装redis-7.0.13过程报错1、没有gcc,报错2、没有python3,报错3、[adlist.o] 错误 127 解决安装报错安装完成 部署redis 主从从结构redis主服务器配置redis启动redis登录redisredis默认是主 redi…...
力扣-338.比特位计数
Idea 直接暴力做法:计算从0到n,每一位数的二进制中1的个数,遍历其二进制的每一位即可得到1的个数 AC Code class Solution { public:vector<int> countBits(int n) {vector<int> ans;ans.emplace_back(0);for(int i 1; i < …...
【Leetcode】 17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 示例 1: 输入:digits "23" 输出&…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
DBAPI如何优雅的获取单条数据
API如何优雅的获取单条数据 案例一 对于查询类API,查询的是单条数据,比如根据主键ID查询用户信息,sql如下: select id, name, age from user where id #{id}API默认返回的数据格式是多条的,如下: {&qu…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
基于 TAPD 进行项目管理
起因 自己写了个小工具,仓库用的Github。之前在用markdown进行需求管理,现在随着功能的增加,感觉有点难以管理了,所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD,需要提供一个企业名新建一个项目&#…...
【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...
「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案
在移动互联网营销竞争白热化的当下,推客小程序系统凭借其裂变传播、精准营销等特性,成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径,助力开发者打造具有市场竞争力的营销工具。 一、系统核心功能架构&…...
