OpenGL 入门(四)—— 贴纸与美颜滤镜

news2025/1/16 12:23:30

本篇我们来介绍贴纸效果与美颜滤镜的实现。

1、贴纸效果

贴纸实际上是一个图片,用 Bitmap 加载图片后用 OpenGL 渲染到指定的位置上。我们举例添加一个耳朵贴纸:

erduo_000

1.1 获取人脸位置

上一篇我们在讲大眼滤镜时,在 Native 层除了获取到人脸 5 个特征点的坐标之外,还保存了人脸起始点坐标与宽高,这个数据其实是我们添加贴纸时才会用到的:

void FaceTracker::detect(const Mat &src, std::vector<Rect2f> &rectangles) {
    std::vector<Rect> faces;
    tracker->process(src);
    tracker->getObjects(faces);

    if (!faces.empty()) {
        // 先只处理一个人脸,将其位置信息保存到 rectangles 中备用
        Rect face = faces[0];
        // 保存人脸起始点(左上角)以及宽高
        rectangles.emplace_back(face.x, face.y, face.width, face.height);

        seeta::ImageData imageData = seeta::ImageData(src.cols, src.rows);
        imageData.data = src.data;

        seeta::FaceInfo faceInfo;
        seeta::Rect bbox;
        bbox.x = face.x;
        bbox.y = face.y;
        bbox.width = face.width;
        bbox.height = face.height;
        faceInfo.bbox = bbox;

        seeta::FacialLandmark landmarks[5];

        faceAlignment->PointDetectLandmarks(imageData, faceInfo, landmarks);

        for (auto & landmark : landmarks) {
            rectangles.emplace_back(landmark.x, landmark.y, 0, 0);
        }
    }
}

该数据最终被封装到 Face 中,通过 landmarks[0]、landmarks[1] 可以获取起始点坐标,通过 faceWidth、faceHeight 获取人脸宽高。

1.2 贴纸滤镜实现

添加贴纸不需要增加新的着色器,大致步骤如下:

  1. 用 Bitmap 加载贴纸资源,在准备阶段创建一个贴纸纹理与该 Bitmap 绑定
  2. 绘制阶段先将贴纸滤镜前面的滤镜绘制的内容绘制出来,然后再绘制贴纸
  3. 绘制贴纸时,重要的是计算好贴纸的起始位置与宽高并设置给 glViewport()

绘制贴纸之前的代码是常规套路前面已经说过多次,这里直接贴出,不再赘述:

class StickFilter(context: Context) :
    BaseFrameFilter(context, R.raw.base_vertex, R.raw.base_fragment) {

    private val mBitmap: Bitmap

    // 绘制贴纸的纹理
    private lateinit var mTextureId: IntArray
    private var mFace: Face? = null

    init {
        mBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.erduo_000)
    }

    override fun initCoordinator() {
        // 转 180° 调正
        val texture = floatArrayOf(
            0.0f, 0.0f,
            1.0f, 0.0f,
            0.0f, 1.0f,
            1.0f, 1.0f
        )
        mTextureBuffer.clear()
        mTextureBuffer.put(texture)
    }

    override fun onReady(width: Int, height: Int) {
        super.onReady(width, height)

        // 1.生成并绑定贴纸的纹理 ID
        mTextureId = IntArray(1)
        TextureHelper.generateTextures(mTextureId)
        glBindTexture(GL_TEXTURE_2D, mTextureId[0])

        // 2.将 Bitmap 的像素数据加载到 OpenGL 的纹理对象中
        GLUtils.texImage2D(GL_TEXTURE_2D, 0, mBitmap, 0)

        // 3.解绑
        glBindTexture(GL_TEXTURE_2D, 0)
    }

    override fun onDrawFrame(textureId: Int): Int {
        // 1.如果数据不足无法绘制贴纸,就返回上一层的纹理 ID
        val landmarks = mFace?.landmarks
        val imgWidth = mFace?.imgWidth ?: 0
        val imgHeight = mFace?.imgHeight ?: 0
        if (landmarks == null || imgWidth == 0 || imgHeight == 0) {
            return textureId
        }

        // 2.渲染前的设置
        // 2.1 设置视窗
        glViewport(0, 0, mWidth, mHeight)

        // 2.2 绑定 FBO
        glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])

        // 2.3 使用着色器程序
        glUseProgram(mProgramId)

        // 3.给顶点着色器的顶点和纹理坐标变量传值
        mVertexBuffer.position(0)
        glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer)
        glEnableVertexAttribArray(vPosition)

        mTextureBuffer.position(0)
        glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer)
        glEnableVertexAttribArray(vCoord)

        // 4.绘制前面滤镜的内容
        // 4.1 激活图层
        glActiveTexture(GL_TEXTURE0)

        // 4.2 绑定纹理
        glBindTexture(GL_TEXTURE_2D, textureId)

        // 4.3 给采样器传参
        glUniform1i(vTexture, 0)

        // 4.4 通知 OpenGL 绘制
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

        // 4.5 解绑 FBO
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glBindTexture(GL_TEXTURE_2D, 0)

        // 5.绘制贴纸
        drawStick(landmarks, imgWidth, imgHeight)

        return mFrameBufferTextures!![0]
    }
}

主要看绘制贴纸的方法 drawStick():

	private fun drawStick(landmarks: FloatArray, imgWidth: Int, imgHeight: Int) {
        // 1.混合模式
        // 1.1 开启混合模式
        glEnable(GL_BLEND)
        // 1.2 设置混合模式:
        // GL_ONE 表示原图全部绘制
        // GL_ONE_MINUS_SRC_ALPHA 表示目标图因子 = 1 - 源图 alpha
        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)

        // 2.设置贴纸的绘制区域
        // 2.1 计算人脸起始点坐标(图像内坐标转换为屏幕内坐标)
        val xInScreen = landmarks[0] / imgWidth * mWidth
        val yInScreen = landmarks[1] / imgHeight * mHeight
        // 2.2 设置贴纸绘制的起始点与宽高
        glViewport(
            xInScreen.toInt(),
            // yInScreen 是人脸的起始点纵坐标,而贴纸需要放在头上,向上移适当距离
            (yInScreen - mBitmap.height / 2).toInt(),
            // 贴纸宽度要根据人脸矩形宽度在屏幕内等比例缩放,记得先用 Float 计算否则误差较大
            ((mFace?.faceWidth ?: 0).toFloat() / imgWidth * mWidth).toInt(),
            mBitmap.height
        )

        // 3.绑定 FBO、设置着色器程序
        glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])
        glUseProgram(mProgramId)

        // 4.为顶点坐标和纹理坐标赋值
        mVertexBuffer.position(0)
        glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer)
        glEnableVertexAttribArray(vPosition)

        mTextureBuffer.position(0)
        glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer)
        glEnableVertexAttribArray(vCoord)

        // 5.绘制与解绑
        // 激活图层
        glActiveTexture(GL_TEXTURE0)
        // 绑定
        glBindTexture(GL_TEXTURE_2D, mTextureId[0])
        // 传递参数
        glUniform1i(vTexture, 0)
        // 通知 OpenGL 绘制
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
        // 解绑 FBO
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glBindTexture(GL_TEXTURE_2D, 0)
        // 关闭混合模式
        glDisable(GL_BLEND)
    }

需要注意的就是 2.2 设置绘制的起点与宽高的问题:

  • 如果起始点的纵坐标不向上移动适当距离,那么贴纸就会贴在眼睛上方,而不是头上方
  • 贴纸的宽度需要随着人脸矩形的宽度变化,否则人离屏幕很远的情况下,脸变小了,但贴纸还是原来的大小就很违和

最后将 StickFilter 添加到渲染器的责任链中进行绘制。这里我们做了一点改动,就是在 UI 上添加了各个滤镜的开关,当开启滤镜时,才进行绘制。因此要从 UI 将开启状态经过 FilterSurfaceView 同步给渲染器 GLRender:

	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)

        // 各个滤镜的控制开关
        mBinding.cbBeauty.setOnCheckedChangeListener { _, isChecked ->
            mBinding.myGlSurfaceView.enableBeauty(isChecked)
        }
        mBinding.cbBigEye.setOnCheckedChangeListener { _, isChecked ->
            mBinding.myGlSurfaceView.enableBigEye(isChecked)
        }
        mBinding.cbStick.setOnCheckedChangeListener { _, isChecked ->
            mBinding.myGlSurfaceView.enableStick(isChecked)
        }
    }

FilterSurfaceView 只做简单的传递:

    fun enableBigEye(checked: Boolean) {
        mGLRender.enableBigEye(checked)
    }

    fun enableStick(checked: Boolean) {
        mGLRender.enableStick(checked)
    }

GLRender 收到后要在 OpenGL 的渲染线程中做滤镜对象的创建工作:

	fun enableBigEye(checked: Boolean) {
        mGLSurfaceView.queueEvent {
            if (checked) {
                mBigEyesFilter = BigEyesFilter(mContext)
                // 同步宽高信息
                mBigEyesFilter?.onReady(mWidth, mHeight)
            } else {
                mBigEyesFilter?.release()
                mBigEyesFilter = null
            }
        }
    }

    fun enableStick(checked: Boolean) {
        mGLSurfaceView.queueEvent {
            if (checked) {
                mStickFilter = StickFilter(mContext)
                mStickFilter?.onReady(mWidth, mHeight)
            } else {
                mStickFilter?.release()
                mStickFilter = null
            }
        }
    }

queueEvent() 会将任务提交到 OpenGL ES 渲染线程执行。因为 Android OpenGL ES 的渲染操作必须在渲染线程(也就是 GLSurfaceView 中的 GLThread)上执行,以避免多线程访问 OpenGL ES 上下文导致的竞态条件和不一致性。因此,当我们需要在非渲染线程上执行 OpenGL ES 操作时,就需要使用 queueEvent 来将任务提交到渲染线程执行。

在绘制时也要做相应修改:

	private lateinit var mScreenFilter: ScreenFilter
    private lateinit var mCameraFilter: CameraFilter
    private var mBigEyesFilter: BigEyesFilter? = null
    private var mStickFilter: StickFilter? = null

	override fun onDrawFrame(gl: GL10?) {
        ...
        // 3.交给滤镜进行具体的绘制工作
        mCameraFilter.setMatrix(mMatrix)
        var textureId = mCameraFilter.onDrawFrame(mTextureIds[0])

        mBigEyesFilter?.setFace(mFaceTracker.getFace())
        textureId = mBigEyesFilter?.onDrawFrame(textureId) ?: textureId

        mStickFilter?.setFace(mFaceTracker.getFace())
        textureId = mStickFilter?.onDrawFrame(textureId) ?: textureId

        mScreenFilter.onDrawFrame(textureId)
    }

2、美颜滤镜

美颜滤镜的实现的重点是在美颜算法,算法是写在片元着色器中的,我们先来看其实现。

2.1 美颜着色器

美颜效果主要是通过着色器中的算法代码实现的,算法有很多种,这里我们提供一种算法放在 beauty_fragment.glsl 中:

precision mediump float;

varying mediump vec2 aCoord;

uniform sampler2D vTexture;

// 图片(纹理)宽高
uniform int width;
uniform int height;

// 高斯模糊的 20 个采样点
vec2 blurCoordinates[20];

void main() {
    // 1.高斯模糊
    // 像素点步长
    vec2 singleStepOffset = vec2(1.0 / float(width), 1.0 / float(height));
    // aCoord 是 GPU 当前渲染的像素点, 整个公式就是求出距离当前正在渲染的像素点在
    // X 轴方向左侧 10 个像素单位的点的坐标。后续的采样点也是类似的含义
    blurCoordinates[0] = aCoord.xy + singleStepOffset * vec2(0.0, -10.0);
    blurCoordinates[1] = aCoord.xy + singleStepOffset * vec2(0.0, 10.0);
    blurCoordinates[2] = aCoord.xy + singleStepOffset * vec2(-10.0, 0.0);
    blurCoordinates[3] = aCoord.xy + singleStepOffset * vec2(10.0, 0.0);
    blurCoordinates[4] = aCoord.xy + singleStepOffset * vec2(5.0, -8.0);
    blurCoordinates[5] = aCoord.xy + singleStepOffset * vec2(5.0, 8.0);
    blurCoordinates[6] = aCoord.xy + singleStepOffset * vec2(-5.0, 8.0);
    blurCoordinates[7] = aCoord.xy + singleStepOffset * vec2(-5.0, -8.0);
    blurCoordinates[8] = aCoord.xy + singleStepOffset * vec2(8.0, -5.0);
    blurCoordinates[9] = aCoord.xy + singleStepOffset * vec2(8.0, 5.0);
    blurCoordinates[10] = aCoord.xy + singleStepOffset * vec2(-8.0, 5.0);
    blurCoordinates[11] = aCoord.xy + singleStepOffset * vec2(-8.0, -5.0);
    blurCoordinates[12] = aCoord.xy + singleStepOffset * vec2(0.0, -6.0);
    blurCoordinates[13] = aCoord.xy + singleStepOffset * vec2(0.0, 6.0);
    blurCoordinates[14] = aCoord.xy + singleStepOffset * vec2(6.0, 0.0);
    blurCoordinates[15] = aCoord.xy + singleStepOffset * vec2(-6.0, 0.0);
    blurCoordinates[16] = aCoord.xy + singleStepOffset * vec2(-4.0, -4.0);
    blurCoordinates[17] = aCoord.xy + singleStepOffset * vec2(-4.0, 4.0);
    blurCoordinates[18] = aCoord.xy + singleStepOffset * vec2(4.0, -4.0);
    blurCoordinates[19] = aCoord.xy + singleStepOffset * vec2(4.0, 4.0);

    // 正在渲染(采样)的点,即所有采样点的中心点的颜色矩阵
    vec4 currentColor = texture2D(vTexture, aCoord);

    // 计算 21 个点的颜色总和
    vec3 totalRGB = currentColor.rgb;
    for (int i = 0; i < 20; i++) {
        totalRGB += texture2D(vTexture, blurCoordinates[i].xy).rgb;
    }

    vec4 blur = vec4(totalRGB * 1.0 / 21.0, currentColor.a);

    // 2.高反差保留
    // 用原图减去高斯模糊的图
    // https://shaderific.com/glsl/common_functions.html
    // OpenGL 内置函数参考网站
    vec4 highPassColor = currentColor - blur;
    // clamp 会返回三个参数中大小在中间的那个数
    // 计算强度系数,对每个颜色通道取反向
    highPassColor.r = clamp(2.0 * highPassColor.r * highPassColor.r * 24.0, 0.0, 1.0);
    highPassColor.g = clamp(2.0 * highPassColor.g * highPassColor.g * 24.0, 0.0, 1.0);
    highPassColor.b = clamp(2.0 * highPassColor.b * highPassColor.b * 24.0, 0.0, 1.0);

    vec4 highPassBlur = vec4(highPassColor.rgb, 1.0);

    // 3.磨皮(融合)
    // 蓝色分量
    float blue = min(currentColor.b, blur.b);
    float value = clamp((blue - 0.2) * 5.0, 0.0, 1.0);

    // 取 RGB 三个分量重最大的值
    float maxChannelColor = max(max(currentColor.r, currentColor.g), currentColor.b);

    // 磨皮强度
    float intensity = 1.0;
    float currentIntensity = (1.0 - maxChannelColor / (maxChannelColor + 0.2)) * value * intensity;

    // mix 返回线性混合的 xy,如 x(1 - a) + ya
    vec3 r = mix(currentColor.rgb, blur.rgb, currentIntensity);

    gl_FragColor = vec4(r, 1.0);
}

2.2 美颜滤镜

滤镜代码前面已经添加过几次,都是固定套路了:

/**
 * 美颜:反向、高反差保留、高斯模糊
 */
class BeautyFilter(context: Context) :
    BaseFrameFilter(context, R.raw.base_vertex, R.raw.beauty_fragment) {

    // 着色器中定义的宽高变量
    private val width: Int = glGetUniformLocation(mProgramId, "width")
    private val height: Int = glGetUniformLocation(mProgramId, "height")


    override fun initCoordinator() {
        val texture = floatArrayOf(
            0.0f, 0.0f,
            1.0f, 0.0f,
            0.0f, 1.0f,
            1.0f, 1.0f
        )
        mTextureBuffer.clear()
        mTextureBuffer.put(texture)
    }

    override fun onDrawFrame(textureId: Int): Int {
        // 1.设置视窗
        glViewport(0, 0, mWidth, mHeight)
        // 绑定 FBO
        glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers!![0])

        // 2.使用着色器程序
        glUseProgram(mProgramId)

        // 3.为着色器中定义的变量赋值
        mVertexBuffer.position(0)
        glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer)
        glEnableVertexAttribArray(vPosition)

        mTextureBuffer.position(0)
        glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer)
        glEnableVertexAttribArray(vCoord)

        glUniform1i(width, mWidth)
        glUniform1i(height, mHeight)

        // 4.后续常规操作,OpenGL 绘制
        // 激活图层
        glActiveTexture(GL_TEXTURE0)
        // 绑定
        glBindTexture(GL_TEXTURE_2D, textureId)
        // 传递参数
        glUniform1i(vTexture, 0)
        // 通知 OpenGL 绘制
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
        // 解绑 FBO
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        glBindTexture(GL_TEXTURE_2D, 0)

        return mFrameBufferTextures!![0]
    }
}

最后在渲染器中添加美颜滤镜:

	private var mBeautyFilter: BeautyFilter? = null
	
    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        ...
        mBeautyFilter?.onReady(width, height)
        ...
    }

    override fun onDrawFrame(gl: GL10?) {
        ...
        // 3.交给滤镜进行具体的绘制工作
        mCameraFilter.setMatrix(mMatrix)
        var textureId = mCameraFilter.onDrawFrame(mTextureIds[0])

        mBigEyesFilter?.setFace(mFaceTracker.getFace())
        textureId = mBigEyesFilter?.onDrawFrame(textureId) ?: textureId

        mStickFilter?.setFace(mFaceTracker.getFace())
        textureId = mStickFilter?.onDrawFrame(textureId) ?: textureId

        textureId = mBeautyFilter?.onDrawFrame(textureId) ?: textureId

        mScreenFilter.onDrawFrame(textureId)
    }

	fun enableBeauty(checked: Boolean) {
        mGLSurfaceView.queueEvent {
            if (checked) {
                mBeautyFilter = BeautyFilter(mContext)
                // 同步宽高信息
                mBeautyFilter?.onReady(mWidth, mHeight)
            } else {
                mBeautyFilter?.release()
                mBeautyFilter = null
            }
        }
    }

结果如文章开头演示所示,至此,滤镜系列完结。

参考资料:

高反差保留算法: https://www.jianshu.com/p/bb702124d2ad

图层混合强光模式:https://blog.csdn.net/matrix_space/article/details/22426633

开源美颜相机工程参考:https://github.com/wuhaoyu1990/MagicCamera

美白着色器代码参考:https://github.com/smzhldr/AGLFramework/blob/master/aglframework/src/main/res/raw/light_f.glsl

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据库表的约束(下)】

文章目录 一、自增长主键二、唯一键约束三、外键约束总结 一、自增长主键 这个约束的功能是&#xff1a; 你不用管我&#xff0c;也不需要插入我这一列的数据&#xff0c;我会保证自己与其他数据不冲突&#xff0c;并且是连续的。 创建一个表&#xff0c;表格内容如下&#x…

Tkinter组件:Checkbutton

Tkinter组件&#xff1a;Checkbutton Checkbutton&#xff08;多选按钮&#xff09;组件用于实现确定是否选择的按钮。Checkbutton 组件可以包含文本或图像&#xff0c;你可以将一个 Python 的函数或方法与之相关联&#xff0c;当按钮被按下时&#xff0c;对应的函数或方法将被…

保护公司机密:避免员工带着数据说拜拜

公司的核心资产之一就是数据。无论是客户信息、研发代码、内部决议、财务报告、商业合同、设计图纸等都是公司的重要资产。如果这些数据在员工离职时被带走&#xff0c;或在员工在职期间不当行为导致数据泄露&#xff0c;将给公司带来重大损失。 然而&#xff0c;保护这些数据…

大模型微调之 在亚马逊AWS上实战LlaMA案例(四)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;四&#xff09; 在 Amazon SageMaker JumpStart 上微调 Llama 2 以生成文本 Meta 能够使用Amazon SageMaker JumpStart微调 Llama 2 模型。 Llama 2 系列大型语言模型 (LLM) 是预先训练和微调的生成文本模型的集合&#x…

漏洞伴随App无时不在,该怎么办?

漏洞攻击、加密被破坏以及数据泄露是App面临的三大重要安全风险&#xff0c;无论开发者如何防范&#xff0c;攻击者往往会找到新的方法&#xff0c;并依靠最新的工具来破坏App安全性。 统计数据表明&#xff0c;大约82&#xff05;的漏洞是在App程序代码中发现的。如果尽快发现…

百度百科怎么修改词条

百度百科是一个由网民编辑内容的网络百科全书&#xff0c;支持用户添加或修改词条。以下是关于如何修改百度百科词条的详细步骤和注意事项。 1. 登录百度百科账户 首先&#xff0c;你需要登录到百度百科账户。如果你还没有百度账号&#xff0c;你需要先注册一个。注册完成后&a…

【vue+el-upload】当action=“#“,代表不使用默认上传,使用自定义上传,http-request获取文件流

el-upload有多种上传行为&#xff1a; 1、立即上传&#xff1a; 当 action 属性被赋予一个有效的 URL 时&#xff0c;一旦用户选择了文件&#xff0c;el-upload 组件会立即自动将文件上传到指定的服务器地址。 2、不立即上传&#xff08;自定义触发&#xff09;&#xff1a; 如…

2024-5-3学习笔记 虚拟继承原理

目录 原理 总结 前面提到过&#xff0c;解决菱形继承产生的数据二义性问题和数据冗余&#xff0c;就需要用到虚拟继承&#xff0c;关于它是如何解决的&#xff0c;我们来一起研究。 class Person { public :string _name ; // 姓名 }; class Student : virtual public Perso…

微软exchange邮箱发送

使用java发送exchange类型的邮件&#xff0c;foxmail中配置如下图&#xff1a; 需要的maven依赖如下&#xff1a; <dependency><groupId>com.microsoft.ews-java-api</groupId><artifactId>ews-java-api</artifactId><version>2.0</ve…

以gitee为例的git入门使用指北

安装git 在linux中我们首先需要使用 sudo apt install git来下载git 在windows中可以下载msysGit 链接&#xff1a;https://git-scm.com/download/win gitee准备 申请账号 建立仓库 ​ 点击新建仓库 这里一般是私有库&#xff0c;点击创建&#xff0c;这时你就拥有一个线上…

LearnOpenGL(九)之材质

一、材质 在现实世界里&#xff0c;每个物体会对光产生不同的反应。比如&#xff0c;钢制物体看起来通常会比陶土花瓶更闪闪发光&#xff0c;一个木头箱子也不会与一个钢制箱子反射同样程度的光。在opengl中&#xff0c;我们可以针对每种表面定义不同的材质(Material)属性来模…

Web自动化测试入门:前端页面的组成分析详解!

在进行Web自动化测试时&#xff0c;了解前端页面的组成是非常重要的&#xff0c;因为页面的不同元素和交互会直接影响测试的实施和结果。本文将从0到1详细规范地介绍前端页面的组成。 1. 页面结构 一个典型的前端页面通常由HTML、CSS和JavaScript三部分组成。HTML负责页面的整…

STL速查

容器 (Containers) 图解容器 支持随机访问 stringarrayvectordeque支持支持支持支持 string 类 构造函数 string(); ------创建一个空的字符串 例如: string str;string(const char* s); ------使用字符串s初始化string(const string& str); ------拷贝构造 赋值操作…

Spring中FactoryBean的作用和实现原理

Spring中FactoryBean的作用和实现原理 BeanFactory与FactoryBean&#xff0c;相信很多刚翻看Spring源码的同学跟我一样很好奇这俩货怎么长得这么像&#xff0c;分别都是干啥用的。 BeanFactory是Spring中Bean工厂的顶层接口&#xff0c;也是我们常说的SpringIOC容器&#xff…

pip install flash_attn 报错

目录 报错截图关键问题nvcc -V 查看 cuda 版本查看 usr/local/cuda-* 安装的cuda版本设置 cuda-12.0 &#xff08;添加入环境变量&#xff09;FlashAttention 安装成功 报错截图 ImportError: This modeling file requires the following packages that were not found in you…

OSPF Stub区域

原理概述 OSPF 协议定义了多种区域&#xff08; Area &#xff09;类型&#xff0c;其中比较常见的有 Stub 区域和 Totally Stub 区域。区域的类型决定了在这个区域当中所存在的 LSA 的类型。 Stub 区域不允许 Type-4和 Type-5 LSA 进入&#xff0c;该区域会通过 Type-3 LSA…

Elasticsearch中的三种分页策略深度解析:原理、使用及对比

码到三十五 &#xff1a; 个人主页 在Elasticsearch中&#xff0c;分页是查询操作中不可或缺的一部分。随着数据量的增长&#xff0c;如何高效地分页查询数据急需需要面对的问题。Elasticsearch提供了三种主要的分页方式&#xff1a;from size、scroll和search_after。下面详细…

【Kubernetes集群一主二从安装教程】

文章目录 环境准备主机间做信任安装ansible工具 升级内核版本使用elrepo源升级内核查看最新版内核安装最新的内核版本设置系统默认内核设置默认内核为我们刚才升级的内核版本 初始化关闭防火墙关闭selinux关闭swap修改主机名修改hosts文件将桥接的IPv4流量传递到iptables的链配…

谈谈IP地址

IP地址 IP地址概念动态分配 IP(DHCP)NAT机制(网络转换机制)IPv6 IP地址组成特殊的IP地址 IP地址 IP协议报文结构: 概念 IP地址: 描述了主机的具体位置.有32位,利用点分十进制的方式来表示.例如: 192.168.190.77 32位ip地址表示的数据非常有限,42亿九千万…, 那么ip地址不够用…

The Sandbox 案例|Web3 项目引领娱乐业的发展

Web3 如何通过 RZR 系列等项目开创娱乐新纪元。 我们已经看到技术和 Web3 如何颠覆金融和银行等行业&#xff0c;然而娱乐业在不断变化的环境中似乎发展滞后。传统的制片厂生态系统、高成本制作以及历史悠久的运作模式一直占据主导地位&#xff0c;而 Web3 项目的出现为创作者提…