WebGL颜色合成

940 阅读5分钟

这是一个五彩缤纷的世界,彩色的玻璃让窗外的风景绚丽夺目。

image-20230422225436621

我们可以把世界想象成一张画布,这个世界里存在着两种风景:

  • source:即将落笔的风景,比如彩色的玻璃。
  • destination:已经落笔的风景,比如窗外的城市。

当这两种风景合到一起后,便会形成新的destination,进入我们的眼睛。

在webgl 中有两种合成颜色的方法:

  • blendFunc(sfactor, dfactor) 基于sfactor, dfactor对source和destination的rgba颜色做合成。
  • blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha) 基于srcRGB, dstRGB对source和destination的rgb颜色做合成;基于srcAlpha, dstAlpha对source和destination的alpha透明度做合成。

blendFunc(sfactor, dfactor) 适合对没有透明度的颜色做合成;

blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha) 适合对有透明度的颜色做合成。

其中的参数需要按照下面的Constant来写。

ConstantFactorDescription
gl.ZERO0,0,0,0所有颜色乘 0.
gl.ONE1,1,1,1所有颜色乘 1.
gl.SRC_COLORRS, GS, BS, AS将所有颜色乘上源颜色。
gl.ONE_MINUS_SRC_COLOR1-RS, 1-GS, 1-BS, 1-AS每个源颜色所有颜色乘 1 .
gl.DST_COLORRD, GD, BD, AD将所有颜色与目标颜色相乘。
gl.ONE_MINUS_DST_COLOR1-RD, 1-GD, 1-BD, 1-AD将所有颜色乘以 1 减去每个目标颜色。
gl.SRC_ALPHAAS, AS, AS, AS将所有颜色乘以源 alpha 值。
gl.ONE_MINUS_SRC_ALPHA1-AS, 1-AS, 1-AS, 1-AS将所有颜色乘以 1 减去源 alpha 值。
gl.DST_ALPHAAD, AD, AD, AD将所有颜色与目标 alpha 值相乘。
gl.ONE_MINUS_DST_ALPHA1-AD, 1-AD, 1-AD, 1-AD将所有颜色乘以 1 减去目标 alpha 值。
gl.CONSTANT_COLORRC, GC, BC, AC将所有颜色乘以一个常数颜色。
gl.ONE_MINUS_CONSTANT_COLOR1-RC, 1-GC, 1-BC, 1-AC所有颜色乘以 1 减去一个常数颜色。
gl.CONSTANT_ALPHAAC, AC, AC, AC将所有颜色乘以一个常数。
gl.ONE_MINUS_CONSTANT_ALPHA1-AC, 1-AC, 1-AC, 1-AC所有颜色乘以 1 减去一个常数。
gl.SRC_ALPHA_SATURATEmin(AS, 1 - AD), min(AS, 1 - AD), min(AS, 1 - AD), 1将 RGB 颜色乘以源 alpha 值或 1 减去目标 alpha 值中的较小值。alpha 值乘以 1.

我们详细说一下上面的两个合成方法。

blendFunc(sfactor, dfactor) 的计算原理如下:

BlendColor=source*sfactor+destination*dfactor

举个例子。

已知:

  • source是(0,0,1,1)
  • destination是(1,1,0,1)
  • sfactor是gl.ONE
  • dfactor是gl.ZERO

求:BlendColor

解:

BlendColor=(0,0,1,1)*(1,1,1,1)+(1,1,0,1)*(0,0,0,0)
BlendColor=(0,0,1,1)

上面的算法就是常见的颜色覆盖,就像画水粉一样,顶上的source压住下面的dfactor。

我们可以写个webgl测试一下。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>颜色合成</title>
        <style>
            body {
                margin: 0;
                overflow: hidden;
            }
        </style>
    </head>

    <body>
        <canvas id="canvas"></canvas>
        <!-- 顶点着色器 -->
        <script id="vertexShader" type="x-shader/x-vertex">
            attribute vec4 a_Position;
            attribute vec4 a_Color;
            varying vec4 v_Color;
            void main(){
                //点位
                gl_Position=a_Position;
                //尺寸
                gl_PointSize=300.0;
                v_Color=a_Color;
            }
        </script>
        <!-- 片元着色器 -->
        <script id="fragmentShader" type="x-shader/x-fragment">
            precision mediump float;
            varying vec4 v_Color;
            void main(){
                gl_FragColor=v_Color;
            }
        </script>
        <script>
            const canvas = document.querySelector('#canvas')
            canvas.width = window.innerWidth
            canvas.height = window.innerHeight

            // 获取着色器文本
            const vsSource = document.querySelector('#vertexShader').innerText
            const fsSource = document.querySelector('#fragmentShader').innerText

            //三维画笔
            const gl = canvas.getContext('webgl')
            gl.enable(gl.BLEND)
            gl.blendFunc(gl.ONE, gl.ZERO)

            //初始化着色器
            initShaders(gl, vsSource, fsSource)
            //声明颜色 rgba
            gl.clearColor(0, 0, 0, 1)

            //如何向attribute 变量中写入多点,并绘制多点
            //顶点数据
            const vertices = new Float32Array([0, 0.2, -0.2, 0])
            //缓冲对象
            const vertexBuffer = gl.createBuffer()
            //绑定缓冲对象
            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
            //写入数据
            gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
            //获取attribute 变量
            const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
            //修改attribute 变量
            gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
            //赋能-批处理
            gl.enableVertexAttribArray(a_Position)

            //颜色数据
            const colors = new Float32Array([
                //蓝色
                0, 0, 1, 1,
                //黄色
                1, 1, 0, 1,
            ])
            //缓冲对象
            const colorBuffer = gl.createBuffer()
            //绑定缓冲对象
            gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
            //写入数据
            gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW)
            //获取attribute 变量
            const a_Color = gl.getAttribLocation(gl.program, 'a_Color')
            //修改attribute 变量
            gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, 0, 0)
            //赋能-批处理
            gl.enableVertexAttribArray(a_Color)

            //刷底色
            gl.clear(gl.COLOR_BUFFER_BIT)

            //绘制顶点
            gl.drawArrays(gl.POINTS, 0, 2)

            // 初始化着色器
            function initShaders(gl, vsSource, fsSource) {
                //创建程序对象
                const program = gl.createProgram()
                //建立着色对象
                const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
                const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
                //把顶点着色对象装进程序对象中
                gl.attachShader(program, vertexShader)
                //把片元着色对象装进程序对象中
                gl.attachShader(program, fragmentShader)
                //连接webgl上下文对象和程序对象
                gl.linkProgram(program)
                //启动程序对象
                gl.useProgram(program)
                //将程序对象挂到上下文对象上
                gl.program = program
                return true
            }
            function createProgram(gl, vsSource, fsSource) {
                //创建程序对象
                const program = gl.createProgram()
                //建立着色对象
                const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
                const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
                //把顶点着色对象装进程序对象中
                gl.attachShader(program, vertexShader)
                //把片元着色对象装进程序对象中
                gl.attachShader(program, fragmentShader)
                //连接webgl上下文对象和程序对象
                gl.linkProgram(program)
                return program
            }

            function loadShader(gl, type, source) {
                //根据着色类型,建立着色器对象
                const shader = gl.createShader(type)
                //将着色器源文件传入着色器对象中
                gl.shaderSource(shader, source)
                //编译着色器对象
                gl.compileShader(shader)
                //返回着色器对象
                return shader
            }
        </script>
    </body>
</html>

效果如下:

image-20230424155511568

那如果黄色是半透明的会怎么样呢?

改下代码:

const colors = new Float32Array([
    //蓝色
    0, 0, 1, 1,
    //黄色
    // 1, 1, 0, 1,
    // 半透明的黄
    1, 1, 0, 0.5,
])

效果如下:

image-20230424160510828

相信你可以看出,黄色变淡了。

这是为什么呢?用之前的公式印证一番即可:

BlendColor=(0,0,1,0.5)*(1,1,1,1)+(1,1,0,1)*(0,0,0,0)
BlendColor=(0,0,1,0.5)

由上可知,合成颜色的透明度变了。

但为什么合成颜色的透明度变了会导致颜色变淡呢?

这跟webgl颜色与canvas画布背景色的合成方式有关,其具体算法我先不细说,大家先知道颜色的透明度会影响颜色的最终渲染效果即可。

接下来,我们还要解决一个问题。

既然黄色半透明了,那黄色与蓝色相交的部分就应该有所变化。

这样我们就得换一套合成因子了:

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

效果如下:

image-20230424162553760

其算法如下:

BlendColor=(0,0,1,0.5)*0.5+(1,1,0,1)*(1-0.5)
BlendColor=(0,0,0.5,0.25)+(0.5,0.5,0,0.5)
BlendColor=(0.5,0.5,0.5,0.75)

通过上面的算法可以看到合成颜色变灰了。

但同时也出现了一个问题:合成区域的透明度变低了。

一个半透明的图像合上一个不透明的图像后,其透明度是不可能降低的。

因此,这个透明度需要与rgb分开计算。

webgl就提供了一个这样的方法-blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha)。

改一下代码,把之前的blendFunc()方法改成blendFuncSeparate()方法。

gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE)

效果如下:

image-20230424165002949

现在的合成颜色就明显比之前颜色暗了。

看一下其算法:

BlendColor.rgb=(0,0,1)*0.5+(1,1,0)*(1-0.5)
BlendColor.rgb=(0,0,0.5)+(0.5,0.5,0)
BlendColor.rgb=(0.5,0.5,0.5)
BlendColor.a=0.5*1+1*1
BlendColor.a=1.5=1

因为透明度的极大值就是1,所以1.5 的透明度依旧是1。

现在关于blendFunc()和blendFuncSeparate()的常规用法,我们就说完了。

我们还可以在其中写入其它的constant,让图像出现不同的合成效果,其作用,大家可以联想一下photoshop里的合成方式:

image-20230424165928940

具体我就不给大家写了,接下来再聊点其它的相关方法。

我们之前在合成source和destination的时候,用的是加法。

BlendColor=source*sfactor+destination*dfactor

其实,我们也可以用减法。

blendEquation(mode):blendFunc()方法中source和destination的合成方式。

  • gl.FUNC_ADD: source + destination (default value)
  • gl.FUNC_SUBTRACT: source - destination
  • gl.FUNC_REVERSE_SUBTRACT: destination - source
  • gl.MIN: Minimum of source and destination
  • gl.MAX: Maximum of source and destination

上面的mode我是从MDN里复制粘贴的,英语很好理解,不翻译了。

既然blendFunc()有了一个blendEquation(),那blendFuncSeparate()也自然有一个blendEquationSeparate()。

blendEquationSeparate(modeRGB, modeAlpha):对应blendFuncSeparate()中rgb和a的两种合成方式,其中的参数类型与blendEquation()一样。

在代码里测试一下。

const gl = canvas.getContext('webgl')
gl.enable(gl.BLEND)
// gl.blendFunc(gl.ONE, gl.ZERO)
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE)
gl.blendEquationSeparate(gl.FUNC_SUBTRACT, gl.FUNC_ADD)

为了方便看减法效果,把蓝色的点改成紫色。

const colors = new Float32Array([
    //蓝色
    // 0, 0, 1, 1,
    // 紫色
    0.5, 0, 1, 1,
    //黄色
    // 1, 1, 0, 1,
    // 半透明的黄
    1, 1, 0, 0.5,
])

效果如下:

image-20230424173623552

相关算法就不写了,理解这个功能就好,免得在three.js的合成里的遇到一堆没见过的单词。

参考链接:

wgld.org/d/webgl/w02…

wgld.org/d/webgl/w03…