【OpenGL ES】EGL+FBO离屏渲染
1 前言
FBO离屏渲染 中使用 GLSurfaceView 来驱动 Renderer 渲染图片,为了隐藏 GLSurfaceView,将其设置为透明的,并且宽高都设置为1。本文将使用 EGL 代替 GLSurfaceView 生成 OpenGL ES 的渲染环境,实现离屏渲染,将渲染后的图片显示在 ImageView 上。
EGL 为 OpenGL ES 提供了绘制表面(或渲染画布),是 OpenGL ES 与显示设备的桥梁,让 OpenGL ES 绘制的内容能够在呈现当前设备上。
EGL 环境创建分为以下5步:
1)创建EGLDisplay
EGLDisplay mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
int[] versions = new int[2];
EGL14.eglInitialize(mEGLDisplay, versions,0, versions, 1);
2)创建EGLConfig
int[] mEGLConfigAttrs = {EGL14.EGL_RED_SIZE, 8,EGL14.EGL_GREEN_SIZE, 8,EGL14.EGL_BLUE_SIZE, 8,EGL14.EGL_ALPHA_SIZE, 8,EGL14.EGL_DEPTH_SIZE, 8,EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] configNum = new int[1];
EGL14.eglChooseConfig(mEGLDisplay, mEGLConfigAttrs, 0, configs, 0,1, configNum, 0);
EGLConfig mEGLConfig = configs[0];
3)创建EGLContext
int[] mEGLContextAttrs = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
EGLContext mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mEGLConfig, EGL14.EGL_NO_CONTEXT, mEGLContextAttrs, 0);
4)创建EGLSurface
int[] eglSurfaceAttrs = {EGL14.EGL_WIDTH, mWidth, EGL14.EGL_HEIGHT, mHeight, EGL14.EGL_NONE};
EGLSurface mEGLSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, eglSurfaceAttrs, 0);
5)绑定EGLSurface和EGLContext到显示设备(EGLDisplay)
EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
读者如果对 OpenGL ES 不太熟悉,请回顾以下内容:
- 绘制三角形
- 绘制立方体
- MVP矩阵变换
透视变换原理
- 纹理贴图
- 正方形图片贴到圆形上
凸镜贴图
FBO离屏渲染
本文完整代码资源见→EGL+FBO离屏渲染
项目目录如下:
2 案例
本案例实现了将彩色图片转换为灰色,并且使用 ImageView 显示转换后的图片。
MainActivity.java
package com.zhyan8.egl.activity;import android.graphics.Bitmap;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import com.zhyan8.egl.R;
import com.zhyan8.egl.model.Model;
import com.zhyan8.egl.opengl.MyEGLSurface;
import com.zhyan8.egl.opengl.MyRender;public class MainActivity extends AppCompatActivity implements Model.Callback {private ImageView mImageView;private MyEGLSurface mEGlSurface;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mImageView = findViewById(R.id.imageView);initEGLSurface();mEGlSurface.requestRender();}private void initEGLSurface() {mEGlSurface = new MyEGLSurface(this);MyRender render = new MyRender(getResources());render.setCallback(this);mEGlSurface.init(render);}@Overridepublic void onCall(final Bitmap bitmap) {runOnUiThread(() -> {mImageView.setImageBitmap(bitmap);});}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#556688"><ImageViewandroid:id="@+id/imageView"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="fitCenter"/></FrameLayout>
BaseEGLSurface.java
package com.zhyan8.egl.opengl;import android.content.Context;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.util.DisplayMetrics;
import android.view.WindowManager;public class BaseEGLSurface {protected EGLDisplay mEGLDisplay;protected EGLConfig mEGLConfig;protected EGLContext mEGLContext;protected EGLSurface mEGLSurface;protected Context mContext;protected Renderer mRenderer;protected EglStatus mEglStatus = EglStatus.INVALID;protected int mWidth;protected int mHeight;public BaseEGLSurface(Context context) {mContext = context;WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics displayMetrics = new DisplayMetrics();mWindowManager.getDefaultDisplay().getRealMetrics(displayMetrics);mWidth = displayMetrics.widthPixels;mHeight = displayMetrics.heightPixels;}public BaseEGLSurface(Context context, int width, int height) {mContext = context;mWidth = width;mHeight = height;}// 设置渲染器public void setRenderer(Renderer renderer) {mRenderer = renderer;}// EGLDisplay宽高发生变化public void onSurfaceChanged(int width, int height) {mWidth = width;mHeight = height;EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);createSurface();mEglStatus = EglStatus.CREATED;}// 请求渲染public void requestRender() {if (mEglStatus == mEglStatus.INVALID) {return;}if (mEglStatus == EglStatus.INITIALIZED) {mRenderer.onSurfaceCreated();mRenderer.onSurfaceChanged(mWidth, mHeight);mEglStatus = EglStatus.CREATED;}if (mEglStatus == EglStatus.CREATED || mEglStatus == EglStatus.DRAW) {mRenderer.onDrawFrame();mEglStatus = EglStatus.DRAW;}}// 创建EGL环境public void createEGLEnv() {createDisplay();createConfig();createContext();createSurface();makeCurrent();}// 销毁EGL环境public void destroyEGLEnv() {// 与显示设备解绑EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);// 销毁 EGLSurfaceEGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);// 销毁EGLContextEGL14.eglDestroyContext(mEGLDisplay, mEGLContext);// 销毁EGLDisplay(显示设备)EGL14.eglTerminate(mEGLDisplay);mEGLContext = null;mEGLSurface = null;mEGLDisplay = null;}// 1.创建EGLDisplayprivate void createDisplay() {mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);int[] versions = new int[2];EGL14.eglInitialize(mEGLDisplay, versions,0, versions, 1);}// 2.创建EGLConfigprivate void createConfig() {EGLConfig[] configs = new EGLConfig[1];int[] configNum = new int[1];EGL14.eglChooseConfig(mEGLDisplay, mEGLConfigAttrs, 0, configs, 0,1, configNum, 0);if (configNum[0] > 0) {mEGLConfig = configs[0];}}// 3.创建EGLContextprivate void createContext() {if (mEGLConfig != null) {mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mEGLConfig, EGL14.EGL_NO_CONTEXT, mEGLContextAttrs, 0);}}// 4.创建EGLSurfaceprivate void createSurface() {if (mEGLContext != null && mEGLContext != EGL14.EGL_NO_CONTEXT) {int[] eglSurfaceAttrs = {EGL14.EGL_WIDTH, mWidth, EGL14.EGL_HEIGHT, mHeight, EGL14.EGL_NONE};mEGLSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, eglSurfaceAttrs, 0);}}// 5.绑定EGLSurface和EGLContext到显示设备(EGLDisplay)private void makeCurrent() {if (mEGLSurface != null && mEGLSurface != EGL14.EGL_NO_SURFACE) {EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);mEglStatus = EglStatus.INITIALIZED;}}// EGLConfig参数private int[] mEGLConfigAttrs = {EGL14.EGL_RED_SIZE, 8,EGL14.EGL_GREEN_SIZE, 8,EGL14.EGL_BLUE_SIZE, 8,EGL14.EGL_ALPHA_SIZE, 8,EGL14.EGL_DEPTH_SIZE, 8,EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,EGL14.EGL_NONE};// EGLContext参数private int[] mEGLContextAttrs = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};// EGL状态enum EglStatus {INVALID, INITIALIZED, CREATED, DRAW}// 渲染器接口interface Renderer {void onSurfaceCreated();void onSurfaceChanged(int width, int height);void onDrawFrame();}
}
MyEGLSurface.java
package com.zhyan8.egl.opengl;import android.content.Context;public class MyEGLSurface extends BaseEGLSurface {public MyEGLSurface(Context context) {super(context);}public MyEGLSurface(Context context, int width, int height) {super(context, width, height);}public void init(Renderer renderer) {setRenderer(renderer);createEGLEnv();}
}
MyRender.java
package com.zhyan8.egl.opengl;import android.content.res.Resources;
import android.opengl.GLES30;
import com.zhyan8.egl.model.Model;public class MyRender implements BaseEGLSurface.Renderer {private Model mModel;public MyRender(Resources resources) {mModel = new Model(resources);}@Overridepublic void onSurfaceCreated() {//设置背景颜色GLES30.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);//启动深度测试GLES30.glEnable(GLES30.GL_DEPTH_TEST);//创建程序idmModel.onModelCreate();}@Overridepublic void onSurfaceChanged(int width, int height) {mModel.onModelChange(width, height);}@Overridepublic void onDrawFrame() {GLES30.glClearColor(0.5f, 0.7f, 0.3f, 1.0f);// 将颜色缓存区设置为预设的颜色GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);// 启用顶点的数组句柄GLES30.glEnableVertexAttribArray(0);GLES30.glEnableVertexAttribArray(1);// 绘制模型mModel.onModelDraw();// 禁止顶点数组句柄GLES30.glDisableVertexAttribArray(0);GLES30.glDisableVertexAttribArray(1);}public void setCallback(Model.Callback callback) {mModel.setCallback(callback);}
}
Model.java
package com.zhyan8.egl.model;import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.opengl.GLES30;
import com.zhyan8.egl.R;
import com.zhyan8.egl.utils.ArraysUtils;
import com.zhyan8.egl.utils.ShaderUtils;
import com.zhyan8.egl.utils.TextureUtils;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;public class Model {private static final int TEXTURE_DIMENSION = 2; // 纹理坐标维度private static final int VERTEX_DIMENSION = 3; // 顶点坐标维度private Callback mCallback;private Resources mResources;private float mVertex[] = {-1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f};private float[] mFboTexture = {0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f};protected FloatBuffer mVertexBuffer;protected FloatBuffer mFboTextureBuffer;// 帧缓冲对象 - 颜色、深度、模板附着点,纹理对象可以连接到帧缓冲区对象的颜色附着点private int[] mFrameBufferId = new int[1];private int[] mTextureId = new int[2];private int mProgramId;private Point mBitmapSize = new Point();public Model(Resources resources) {mResources = resources;mVertexBuffer = ArraysUtils.getFloatBuffer(mVertex);mFboTextureBuffer = ArraysUtils.getFloatBuffer(mFboTexture);}// 模型创建public void onModelCreate() {mProgramId = ShaderUtils.createProgram(mResources, R.raw.vertex_shader, R.raw.fragment_shader);TextureUtils.loadTexture(mResources, R.raw.xxx, mBitmapSize, mTextureId, mFrameBufferId);}// 模型参数变化public void onModelChange(int width, int height) {GLES30.glViewport(0, 0, mBitmapSize.x, mBitmapSize.y);}// 模型绘制public void onModelDraw() {GLES30.glUseProgram(mProgramId);// 准备顶点坐标和纹理坐标GLES30.glVertexAttribPointer(0, VERTEX_DIMENSION, GLES30.GL_FLOAT, false, 0, mVertexBuffer);GLES30.glVertexAttribPointer(1, TEXTURE_DIMENSION, GLES30.GL_FLOAT, false, 0, mFboTextureBuffer);// 激活纹理GLES30.glActiveTexture(GLES30.GL_TEXTURE);// 绑定纹理GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId[0]);// 绑定缓存GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFrameBufferId[0]);// 绘制贴图GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);showBitmap();}private void showBitmap() {// 分配字节缓区大小, 一个像素4个字节ByteBuffer byteBuffer = ByteBuffer.allocate(mBitmapSize.x * mBitmapSize.y * Integer.BYTES);GLES30.glReadPixels(0, 0, mBitmapSize.x, mBitmapSize.y, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, byteBuffer);Bitmap bitmap = Bitmap.createBitmap(mBitmapSize.x, mBitmapSize.y, Bitmap.Config.ARGB_8888);// 从缓存区读二进制缓冲数据bitmap.copyPixelsFromBuffer(byteBuffer);// 回调mCallback.onCall(bitmap);}public void setCallback(Callback callback) {mCallback = callback;}public interface Callback{void onCall(Bitmap bitmap);}
}
ShaderUtils.java
package com.zhyan8.egl.utils;import android.content.res.Resources;
import android.opengl.GLES30;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;public class ShaderUtils {//创建程序idpublic static int createProgram(Resources resources, int vertexShaderResId, int fragmentShaderResId) {final int vertexShaderId = compileShader(resources, GLES30.GL_VERTEX_SHADER, vertexShaderResId);final int fragmentShaderId = compileShader(resources, GLES30.GL_FRAGMENT_SHADER, fragmentShaderResId);return linkProgram(vertexShaderId, fragmentShaderId);}//通过外部资源编译着色器private static int compileShader(Resources resources, int type, int shaderId){String shaderCode = readShaderFromResource(resources, shaderId);return compileShader(type, shaderCode);}//通过代码片段编译着色器private static int compileShader(int type, String shaderCode){int shader = GLES30.glCreateShader(type);GLES30.glShaderSource(shader, shaderCode);GLES30.glCompileShader(shader);return shader;}//链接到着色器private static int linkProgram(int vertexShaderId, int fragmentShaderId) {final int programId = GLES30.glCreateProgram();//将顶点着色器加入到程序GLES30.glAttachShader(programId, vertexShaderId);//将片元着色器加入到程序GLES30.glAttachShader(programId, fragmentShaderId);//链接着色器程序GLES30.glLinkProgram(programId);return programId;}//从shader文件读出字符串private static String readShaderFromResource(Resources resources, int shaderId) {InputStream is = resources.openRawResource(shaderId);BufferedReader br = new BufferedReader(new InputStreamReader(is));String line;StringBuilder sb = new StringBuilder();try {while ((line = br.readLine()) != null) {sb.append(line);sb.append("\n");}br.close();} catch (Exception e) {e.printStackTrace();}return sb.toString();}
}
TextureUtils.java
package com.zhyan8.egl.utils;import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.opengl.GLES30;
import android.opengl.GLUtils;public class TextureUtils {// 加载纹理贴图public static void loadTexture(Resources resources, int resourceId, Point bitmapSize, int[] textureId, int[] frameBufferId) {BitmapFactory.Options options = new BitmapFactory.Options();options.inScaled = false;Bitmap bitmap = BitmapFactory.decodeResource(resources, resourceId, options);bitmapSize.set(bitmap.getWidth(), bitmap.getHeight());// 生成纹理idGLES30.glGenTextures(2, textureId, 0);for (int i = 0; i < 2; i++) {// 绑定纹理到OpenGLGLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId[i]);GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);if (i == 0) {// 第一个纹理对象给渲染管线(加载bitmap到纹理中)GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap, 0);} else {// 第二个纹理对象给帧缓冲区GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap.getWidth(), bitmap.getHeight(),0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);}// 取消绑定纹理GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, GLES30.GL_NONE);}// 创建帧缓存idGLES30.glGenFramebuffers(1, frameBufferId, 0);// 绑定帧缓存GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBufferId[0]);// 将第二个纹理附着在帧缓存的颜色附着点上GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, textureId[1], 0);// 取消绑定帧缓存GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, GLES30.GL_NONE);}
}
ArraysUtils.java
package com.zhyan8.egl.utils;import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;public class ArraysUtils {public static FloatBuffer getFloatBuffer(float[] floatArr) {FloatBuffer fb = ByteBuffer.allocateDirect(floatArr.length * Float.BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();fb.put(floatArr);fb.position(0);return fb;}
}
vertex_shader.glsl
#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec2 aTextureCoord;
out vec2 vTexCoord;
void main() {gl_Position = vPosition;vTexCoord = aTextureCoord;
}
fragment_shader.glsl
#version 300 es
precision mediump float;
uniform sampler2D uTextureUnit;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {vec4 color = texture(uTextureUnit, vTexCoord);float rgb = color.g;vec4 c = vec4(rgb, rgb, rgb, color.a);fragColor = c;
}
3 运行效果
原图:
处理后:
【OpenGL ES】EGL+FBO离屏渲染相关推荐
- android 离屏渲染 简单书,Android OpenGL ES 8.FrameBuffer离屏渲染
作用 FrameBuffer Object,也称FBO,离屏渲染,可以摆脱屏幕的束缚,在后台做图像处理. 理解 FrameBuffer和Texture绑定,FrameBuffer犹如画板,而Textu ...
- OpenGL.ES在Android上的简单实践:23-水印录制(FBO离屏渲染,解决透明冲突,画中画)
OpenGL.ES在Android上的简单实践:23-水印录制(FBO离屏录制,解决透明冲突) 1.水印签名罢工了? 不知道大家有没注意到,之前我们使用MediaCodec录制的视频,水印签名那部分区 ...
- OpenGL ES EGL eglCreatePbufferSurface
目录 一. EGL 前言 二. EGL 绘制流程简介 三.eglCreatePbufferSurface 函数简介 1.eglCreatePbufferSurface 简介 2.eglCreatePb ...
- OpenGL ES EGL 简介
目录 一.EGL 简介 二.EGL 跨平台之 ANGLE 1.ANGLE 支持跨平台 2.ANGLE 支持渲染器 3.ANGLE 下载地址 三.EGL 坐标系 四.EGL 绘图步骤 五.猜你喜欢 零基 ...
- OpenGL ES EGL eglDestroyContext
目录 一. EGL 前言 二. EGL 绘制流程简介 三.eglDestroyContext 函数简介 四.eglDestroyContext 使用 四.猜你喜欢 零基础 OpenGL ES 学习路线 ...
- OpenGL ES EGL eglQueryContext
目录 一. EGL 前言 二. EGL 绘制流程简介 三.eglQueryContext 函数简介 四.eglQueryContext 使用 四.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : ...
- SDL2源码分析之OpenGL ES在windows上的渲染过程
SDL2源码分析之OpenGL ES在windows上的渲染过程 更新于2018年11月4日. 更新于2018年11月21日. ffmpeg + SDL2实现的简易播放器 ffmpeg和SDL非常强大 ...
- OpenGL ES EGL eglDestroySurface
目录 一. EGL 前言 二. EGL 绘制流程简介 三.eglDestroySurface 函数简介 四.eglDestroySurface 使用 四.猜你喜欢 零基础 OpenGL ES 学习路线 ...
- OpenGL ES EGL eglSwapBuffer
目录 一. EGL 前言 二. EGL 绘制流程简介 三.eglSwapBuffer 函数简介 四.关于多个 EGLContext 五.共享 EGLContext 六.猜你喜欢 零基础 OpenGL ...
- WebGL—实现使用FBO离屏渲染(亦同拷贝纹理)off-screen rendering的两种方式
1.离屏渲染使用场景: 1.游戏中的小地图: 2.画中画场景: 3.游戏中观战模式的多场景场合: 4.镜像场景--比如汽车游戏当中的倒车镜,采用的就是离屏渲染技术,在倒车镜上安装一个摄像机,把摄像机渲 ...
最新文章
- 2022-2028年中国酱腌菜行业市场研究及前瞻分析报告
- 半导体物理与器件_上海交通大学874半导体物理2班开课啦!
- c语言每瓶啤酒2元答案,【原创源码】C语言 一个喝啤酒小游戏的编程实现(菜鸟级)...
- java学习-http中get请求的非ascii参数如何编码解码探讨
- word中链接到目标后返回快捷键
- 背完这442句英语,你的口语绝对不成问题了
- 蚂蚁组件 axure 蚂蚁_蚂蚁属性细微差别
- JavaScript 的 defer 与 async
- Java多线程——线程安全问题
- Splash特征描述子
- 好用的android剪辑软件,最好用的视频剪辑app软件有哪些?自媒体人都在用的六款app软件...
- 从王者荣耀看设计模式(十六.原型模式)
- SMAA算法详解 - SMAABlendingWeightCalculationVS
- 2017杭州云栖大会 智能客服专场预热 — 用心服务客户,用云助力客服
- android 音频切换分析,Android音频可视化操作
- 阿里云、蚂蚁区块链医疗解决方案首次应用于未来医院电子处方
- 基于FPGA扰码的实现
- 弹性容器----六大属性(5、项目在交叉轴上的对齐方式)
- 就业困惑!Linux程序员的就业方向
- Spring Cloud Discovery——Apache Zookeeper Discovery