在 html 中用加色法混合颜色
概要
本文通过解决一个假想的问题介绍了 css screen 混合模式,并介绍了如何用 svg 滤镜、canvas 2d、canvas webgl 实现相同的效果。
问题
下面的图片演示三种颜色光叠加的效果,请在 html 中实现这种效果。
约定
词语 | 指代 |
---|---|
混合 | blend |
加色 | additive color - 名词 |
特性 | attribute,比如 <a id=1> ,说 id 是元素 a 的特性 |
透明度 | α、alpha |
伪输入图像 | pseudo input image |
着色器 | shader |
着色器程序 | shader program |
xml 应用程序 | XML application |
chrome | google chrome 41 |
firefox | firefox developer edition 40 |
ie | internet explorer 11 |
3 个浏览器 | 上面 3 个版本的浏览器 |
opera 已经基于 webkit 了,所以未测试 opera,若在 chrome 中可用那我就认为在 opera 中也可用。
分析
当然可以用 photoshop 制作图片,html 用 <img>
引用该图片,本文不讨论这种方法。
观察重叠部分发现该部分的颜色不仅受自己的影响、还受它下面背景颜色的影响,重叠部分的颜色是自己的颜色和背景颜色混合的结果。换句话说,一个像素绘制出来的颜色等于像素颜色和背景像素颜色的混合,即
C=B(Cb,Cs)
,其中,
C
是绘制的颜色
Cb
是背景颜色
Cs
是前景颜色,即像素的颜色
这里面
C
的 r、g、b 颜色分量都是 [0, 1] 的小数而不是 [0, 255] 的整数。显然,对同一个像素来说不同的 rgb(1, 0, 0)
、rgb(0, 1, 0)
、rgb(0, 0, 1)
,
重点:不同的 B 得到不同的
C
不可行的方法
html 中经常用到下面 3 个方法,
- css opacity 属性
- css rgba()/hsla() 颜色
- 用
<img>
引用带 alpha 通道的图像
它们使用相同的混合函数,叫做 α 混合或简单 α 复合,
αs 是前景透明度, αb 是背景透明度,上面的式子计算混合后的 r、g、b 颜色,混合后的透明度 αo 由公式 αo=αs+αb×(1−αs) 给出。很多时候背景不透明,即 αb 是 1,上面把 1 代入了 αb 。
简单 α 复合 - http://dev.w3.org/fxtf/compositing/#simplealphacompositing
opacity - http://stackoverflow.com/questions/8743482/calculating-opacity-value-mathematically
下面给上面的式子代入几组实际值。设
Cs
是不透明红 rgba(1, 0, 0, 1)
,
Cb
是不透明蓝 rgb(0, 0, 1)
,它俩混合的结果不用计算都知道仍然是不透明红,计算过程如下,
r = 1 x 1 + 0 x (1 - 1) = 1
g = 0 x 1 + 0 x (1 - 1) = 0
b = 0 x 1 + 1 x (1 - 1) = 0
红蓝得红,混合失败。另外一组,
Cs
= rgba(1, 0, 0, 0.5)
,
Cb
= rgb(0, 0, 1)
,有,
r = 1 x 0.5 + 0 x (1 - 0.5) = 0.5
g = 0 x 0.5 + 0 x (1 - 0.5) = 0
b = 0 x 0.5 + 1 x (1 - 0.5) = 0.5
要想让得到的 rgb(0.5, 0, 0.5)
和 rgb(0, 1, 0)
的绿色混合以得到 rgb(1, 1, 1)
的白色,α 需要满足下面的方程组,
上面的方程组无解,即无论如何设置 α 都无法通过
B
混合 rgb(0.5, 0, 0.5)
和 rgb(0, 1, 0)
得到 rgb(1, 1, 1)
。
回过头来观察式子
可行的方法
如果可以自己逐一计算像素的颜色,得出要求的效果自然不在话下。除了自己计算外,如果存在正好能够实现要求效果的固定函数,则调用该函数也可以。
在 html 中处理颜色有 3 种工具,css、svg、canvas。
css
css 有个模块叫复合与混合,这个模块定义了若干固定函数,其中一个叫 screen,它的 B 是
css 复合与混合 - http://dev.w3.org/fxtf/compositing/
假设 add 是 min(Cs+Cb,1) ,screen 虽然不是 add 但是也可以把红绿蓝合成白色,实现要求的效果。至于 add、screen 或其它混合函数哪个能更精确地反映光线的混合,我也搞不清楚。
通过指定 html 元素的 css 属性 mix-blend-mode: screen
来让元素和其背后的元素以 screen 方式混合。css 目前没办法逐像素计算目标区域的颜色。
html 中的 svg
本文把 svg 写在 html 内 。svg 是 xml 应用程序,遵循 xml 语法,但是放在 html 中又可以采用部分 html 语法。如果大家按照 xml svg 的知识去看本文的代码可能会有疑问,所以在写 svg 之前先说一下 html 中的 svg。
html 不支持名字空间,忽略
<svg>
里面由特性定义的名字空间,所以本文的 svg 没有xmlns="http://www.w3.org/2000/svg"
或者xmlns:xlink="http://www.w3.org/1999/xlink"
,xlink:href
在 html 中是个普通的特性名,冒号和名字空间无关没有歧义时可以省略特性值周围的引号
xml 中没有内容的元素比如
<circle cx=1 cy=1 r=1></circle>
也可以写做<circle cx=1 cy=1 r=1 />
,叫做自闭合;html 不存在自闭合,但内嵌的 svg 元素可以使用自闭合
html 中的 svg 元素可以自闭合 - http://www.w3.org/TR/html-markup/syntax.html#svg-mathml
所有没有内容的 xml 元素都叫 empty 元素,可以自闭合;html 不存在 empty 元素,但是定义了一些 void 元素,void 元素不能有内容,只有开始标记没有结束标记。
所有 void 元素是,area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr
http://www.w3.org/TR/html-markup/syntax.html#syntax-elements所有 html 元素开始标记的 > 前面可以写一个 /,和不写 / 一样。
<br/>
解释为<br>
而不是<br></br>
,<br>
是 void 元素,所以没问题;<div/>
解释为<div>
,<div>
不是 void 元素,所以可能会出问题,<span style="color: green;"><span style="color: red;"/> red = html, green = xhtml </span>
有些元素可以省略结束标记,但不是 void 元素,比如
<li>
;有些元素有时候没有内容,但既不是 void 元素也不能省略结束标记,比如<script src=xxx>
;有些元素可以省略开始标记。
http://www.w3.org/TR/html5/syntax.html#optional-tags浏览器从网站获取的文件 mimetype = text/html 导致调用 html 解析器。
另外,svg 很多要素都没有浏览器支持;当支持的时候,可能各个浏览器有差异。
有了这些知识下面看 svg。
svg
- svg 里面的元素也是 dom 元素,也可以应用 css 混合。css 混合在 css 部分讲述
- svg 有个规范定义了复合,和 css 混合效果差不多,关键字
comp-op
。我不知道有哪个浏览器支持该规范 - svg 滤镜
<feComposite>
和<feBlend>
svg 滤镜 - http://www.w3.org/tr/svg11/filters.html
svg 复合 - http://www.w3.org/TR/SVGCompositing/
<feComposite>
按其 operator
特性指出的操作组合两个输入图像
i1
、
i2
。当 operator=arithmetic
时需要另外的 4 个特性 k1
、k2
、k3
、k4
,默认值是 0,并按如下方式分别计算结果像素的 3 个通道,我不清楚它如何处理 α 通道,
result=k1×i1×i2+k2×i1+k3×i2+k4⋯(svg.1)
既然知道 mix-blend-mode: screen
的混合函数
B=Cs+Cb−Cs×Cb
,设
Cs
是
i1
,
Cb
是
i2
,有,
所以 <feComposite operator=arithmetic k1=-1 k2=1 k3=1>
可以实现效果。
<feBlend>
支持 screen 混合模式,<feBlend mode=screen>
,所以应该也能实现效果。
canvas
canvas 分为 2d 和 webgl,它里面的形状都是画上去的,由像素组成,不是 dom 元素,无法应用 css 混合;但是 canvas 2d 有个全局复合操作,和 css 混合是同一个概念在两种不同语言中的实现,支持 css 混合的所有固定函数。当然自己计算像素也行。
全局复合操作 - http://dev.w3.org/fxtf/compositing/#canvascompositingandblending
webgl 没有与 css、canvas 2d 完全相同的混合概念,但也有自己的混合函数,解决本文提出的问题不在话下。webgl 有个特点是无论你干什么都需要写着色器代码、写调用编译着色器的函数的代码、写调用连接着色器的函数的代码。
如何运行示例代码
下面是框架代码,后面给出的示例代码需要放在框架代码的 <body>
里
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<style>
.sample { display: inline-block; vertical-align: top; width: 200px; }
</style>
<title>additive color</title>
</head>
<body>
</body>
</html>
依次执行下面 3 个步骤,缺一不可,
- 新建一个空 html 文件
- 拷贝框架代码,粘贴到空的 html 文件
- 确保 html 文件编码为 utf8,保存
示例 - css mix-blend-mode
ie 不认识 mix-blend-mode
和 isolation
mix-blend-mode - http://dev.w3.org/fxtf/compositing/#mix-blend-mode
isolation - http://dev.w3.org/fxtf/compositing/#isolation
元素的 mix-blend-mode
属性是说,我知道你的颜色,但是在显示的时候不要只显示你的颜色,而是要显示你的颜色和你背景颜色混合后的颜色,至于如何混合,我会通过 mix-blend-mode
属性的值指出。
因此设计一个容器 div position: relative
,里面有红绿蓝三个方块 div position: absolute
,三个方块之间有重叠部分,通过 mix-blend-mode: screen
指出重叠部分颜色的计算方法。
<div class=sample>
<style>
.s1 { height: 180px; isolation: isolate; position: relative; }
.s1 > div { height: 100px; mix-blend-mode: screen; position: absolute; width: 100px; }
.s1 > div:nth-of-type(1) { background-color: red; left: 50px; top: 20px; }
.s1 > div:nth-of-type(2) { background-color: lime; left: 30px; top: 40px; }
.s1 > div:nth-of-type(3) { background-color: blue; left: 70px; top: 60px; }
</style>
<div class=s1><div></div><div></div><div></div></div>
<h4>mix-blend-mode: screen 不是颜色分量相加</h4>
</div>
上面的代码实现了刚才的设计,并且额外设置了容器 div 的一个属性 isolation: isolate
。元素的 isolation: isolate
是说,我的子元素不会和我外面的元素混合。isolation
属性另外一个可能的取值兼默认值是 auto
,没有限制、随便混和。
容器 div 放在 html 的 <body>
里,<body>
默认的颜色是不透明白,假设没有通过 isolation: isolate
限定容器元素的子元素不能与容器外的元素混合,红绿蓝 3 个方块 div 就要和白色以 screen 模式混合。红色 rgb(1, 0, 0)
和白色 rgb(1, 1, 1)
以 screen 模式
B=Cs+Cb−Cs×Cb
混合的结果是 rgb(1, 1, 1)
白色,
r = 1 + 1 - 1 x 1 = 1
g = 0 + 1 - 0 x 1 = 1
b = 0 + 1 - 0 x 1 = 1
绿蓝方块和白色混合也得到白色,结果就是一片白,不是要求的效果,所以设置容器的 isolation: isolate
。
示例 - svg <feComposite>
和 <feBlend>
- svg filter primitive 必须包含在
<filter>
元素内 <feComposite>
是 svg filter primitive- 所以?
<filter x=a y=b width=c height=d>
定义一个矩形滤镜区域,默认值是 x=-10%
、y=-10%
、width=120%
、height=120%
,x
和 y
的值相对于应用滤镜的元素,x=-10 y=10
以应用滤镜的元素为准向左 10 向下 10。x
、y
、width
、height
的数值的解释由另外一个特性 filterUnits 决定,
如果 filterUnits
是默认值 objectBoundingBox
x=10
是说 x 是应用滤镜的元素的宽度的 10 倍x=100%
是说 x 是应用滤镜的元素的宽度的 1 倍
如果 filterUnits=userSpaceOnUse
x=10
是说 x 是 10 个用户单位,用户单位具体是啥要看包含这个元素的 svg 的宽度或高度用的单位,默认 pxx=100%
含义不变
filterUnits - http://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion
这个连接指向 15.5 Filter effects region,因为指向 filterUnits 的连接打开后其内容只是一个指向 FilterEffectsRegion 的连接
如果 svg 宽度单位是 px,高度单位是 cm,用户单位是啥?
这个问题我着实回答不上来。难道 x 对应 svg 的 width,y 对应 svg 的 height?
<feComposite in=i1 in2=i2 operator=arithmetic k1=a k2=b k3=c k4=d>
接受两个图像 i1
和 i2
,<feComposite>
逐一扫描 i1
和 i2
的像素,用
svg.1
产生新像素,放置到滤镜区域相应的位置。
<feBlend in=i1 in2=i2 mode=screen>
对图像 i1
和 i2
应用 screen 混合模式。
in
的默认值是 <filter>
中上一个 filter primitive 的结果;如果自己是第一个,则默认 SourceGraphic
。in=SourceGraphic in2=BackgroundImage
分别使用当前图片和当前图片的背景图片。
in - http://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveInAttribute
同样是图片,为啥一个叫 source graphic,一个叫 background image?
http://www.w3.org/TR/SVG11/filters.html#AccessingBackgroundImage
简而言之出于性能考虑伪输入图像
BackgroundImage
是背景的一个快照,要使用BackgroundImage
作in
或in2
的参数必须指定容器元素的enable-background=new
以要求容器存储背景快照供滤镜使用。enable-background
默认值是accumulate
,不存,也无法使用BackgroundImage
。两个不同的单词可能是要强调输入图像和背景图像的这种差异。当然也可能是我想多了,人家就是喜欢出其不意,如之奈何?
混合 3 个形状需要应用两次滤镜,出于演示的目的正好一个用 <feComposite>
一个用 <feBlend>
。
- 创建 3 个分别是红绿蓝的 svg 形状
- 为了使用
BackgroundImage
,把这 3 个形状放到一个组里面,并设置组的enable-background=new
- 放置第 1 个形状
- 把形状 2 放到和形状 1 部分重叠的位置,此时形状 1 可以视为形状 2 的背景,对形状 2 应用滤镜
<feComposite in2=BackgroundImage>
,重叠的部分就会经过 svg.1 计算 - 形状 3 用
<feBlend in2=BackgroundImage>
。
<div class=sample>
<svg height=180 width=200>
<filter id=s2-composite x=0 y=0 width=1 height=1>
<feComposite in2=BackgroundImage operator=arithmetic k1=-1 k2=1 k3=1></feComposite>
</filter>
<filter id=s2-blend>
<feBlend in2=BackgroundImage mode=screen></feBlend>
</filter>
<g enable-background=new>
<rect width=100 height=100 x=50 y=20 fill=red></rect>
<rect width=100 height=100 x=30 y=40 fill=lime filter=url(#s2-composite)></rect>
<rect width=100 height=100 x=70 y=60 fill=blue filter=url(#s2-blend)></rect>
</g>
</svg>
<h4>svg,仅限 ie 10+</h4>
</div>
没有指出 <filter id=s2-blend>
的 x
、y
、width
、height
所以它们都取默认值。
只有 ie 10+ 支持上面的代码。ie 此刻又迸射出耀眼的光芒,
别的浏览器玩儿蛋去吧!
这是我心里想象的 ie 工作人员心里的想象,请不要以为他们一定是那样想的。
Appendix A: The deprecated enable-background property
http://dev.w3.org/fxtf/filters/#AccessBackgroundImagesvg 最近的风向是不赞成
enable-background
了,enable-background=new
要换成isolation=isolate
以“兼容 css 复合与混合”。大家留意一下,isolation
是 css 属性,在样式表里面指定;svg 发明了个presentation attribute
,这种特性也可以在样式表中以 css 属性的形式指定以兼容 css,而 svg 这个isolation
不是所谓的presentation attribute
,不能在样式表里指定。这还兼容个毛?svg 就是这样,当你拿它和 html 比的时候,一眼看上去都差不多,似乎能很容易混用,实际上有很多出其不意的不一样,烦得要死。无论如何还是要换一下试试。换成
isolation=isolate
后连 ie 都没法读取背景图片了,前面的 css 部分说过 ie 不认识样式表中的isolation
,现在看来 ie 也不认识isolation
特性,3 个浏览器没有能运行的。http://dev.w3.org/fxtf/filters/ 里面的示例 Example of feComposite 就是从 http://www.w3.org/TR/SVG11/filters.html 拷贝的同名示例,只是把enable-background=new
换成了isolation=isolate
,但是紧跟其后的连接 View this example as SVG 指向的 svg 文件里面用的仍然是enable-background=new
。这么看来 ie 是被坑了,但从大的方面看 svg 本身就很坑,废弃
enable-background
一点都不亏。svg 很好玩,但是能别用 xml 语法吗?
一般只要 chrome 和 firefox 能用,ie 我常常忽略,前面 css 的示例代码就没管 ie。现在这段代码,由于只有 ie 10+ 支持伪输入图像 BackgroundImage
,chrome 和 firefox 上都运行不了,不能说是达到了要求的效果,最好有在 3 个浏览器上都能运行的 svg 例子。所幸对于这个简单的问题,TIMTOWTDI!下面凑一个能在 3 个浏览器上运行的 svg 解法,说凑是因为它没有把 3 个元素两两混合,而是在滤镜里生成了两个方块,和应用滤镜的那个方块元素混合。在滤镜里生成方块指的是把滤镜矩形设置为单一的颜色然后临时保存,这要用到 <feColorMatrix>
,
<feColorMatrix type=matrix values="
a00 a01 a02 a03 a04
a10 a11 a12 a13 a14
a20 a21 a22 a23 a24
a30 a31 a32 a33 a34" />
说的是当把图像传递给 <feColorMatrix>
时,对图像的每一个像素左乘下面的矩阵以得到新的像素,
a00 a01 a02 a03 a04
a10 a11 a12 a13 a14
a20 a21 a22 a23 a24
a30 a31 a32 a33 a34
0 0 0 0 1 - 最后一行总是它,不写在 values 里
即新像素 (r', g', b', a')
等于 <feColorMatrix>
给出的矩阵乘以原像素 (r, g, b, a)
,
<feColorMatrix type=matrix values="
0 0 0 0 1
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0" />
把 (r, g, b, a, 1)
变成 (1, 0, 0, a, 1)
,这是个红色方块。因此可以写下面的代码,
- 定义一个蓝方块
<rect fill=blue>
- 对蓝方块应用滤镜
- 滤镜从蓝方块生成一个红方块和一个绿方块,偏移,混合
- 上一步的结果和蓝方块混合,蓝方块在滤镜中通过伪输入图像
SourceGraphic
引用
<div class=sample>
<svg height=180 width=200>
<filter id=s2-2 x=-1 y=-1 width=2 height=2>
<feOffset dx=-20 dy=-40></feOffset>
<feColorMatrix result=red type=matrix values="
0 0 0 0 1
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0"></feColorMatrix>
<feOffset in=SourceGraphic dx=-40 dy=-20></feOffset>
<feColorMatrix type=matrix values="
0 0 0 0 0
0 0 0 0 1
0 0 0 0 0
0 0 0 1 0"></feColorMatrix>
<feBlend mode=screen in2=red></feBlend>
<feBlend mode=screen in2=SourceGraphic></feBlend>
</filter>
<rect x=70 y=60 width=100 height=100 fill=blue filter=url(#s2-2)></rect>
</svg>
<h4>svg feBlend</h4>
</div>
也可以做红绿蓝 3 张图片,在滤镜里用 <feImage>
引用,代码类似下面,记得先做 3 张 100px * 100px 的红绿蓝图片放到 html 的同一目录。
<svg width=200 height=180>
<filter id=additive>
<feImage x=50 y=20 width=100 height=100 result=layer1 xlink:href=05-red.png />
<feImage x=30 y=40 width=100 height=100 result=layer2 xlink:href=05-lime.png />
<feImage x=70 y=60 width=100 height=100 result=layer3 xlink:href=05-blue.png />
<feBlend in=layer1 in2=layer2 mode=screen result=step-1 />
<feBlend in=step-1 in2=layer3 mode=screen />
</filter>
<rect width=200 height=180 filter=url(#additive) />
</svg>
总结
svg 滤镜的思路就是 <feComposite>
和 <feBlend>
,前者自己计算像素,后者调用固定函数。由于 chrome 和 firefox 不支持在滤镜中读取背景图像所以给了两段绕弯的代码,第 2 段代码还依赖 3 张图片。
示例 - canvas 2d
方法 1. CanvasRenderingContext2D.prototype.globalCompositeOperation
globalCompositeOperation 是 HTML Canvas 2D Context 规范定义在接口 CanvasRenderingContext2D 上的一个特性。chrome 里面访问不到 CanvasRenderingContext2D.prototype.globalCompositeOperation,firefox 和 ie 里面存在该 js 属性,定义了 get 和 set 访问函数。
不能直接在代码里使用
CanvasRenderingContext2D.prototype.globalCompositeOperation
,因为这句代码会调用get
函数,而get
需要通过this
访问实际的画布上下文对象。当然正常情况下也不会那么写,正常情况是先获取某个画布的 2d 上下文,然后访问上下文的属性,var t = theCanvas.getContext("2d"); console.log(t.globalCompositeOperation); // 默认 "source-over"
有了上面的
t
,可以写
Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, "globalCompositeOperation").get.call(t);
interface CanvasRenderingContext2D
http://www.w3.org/TR/2dcontext/#canvasrenderingcontext2d
- 绘制 红 色方块
- 全局复合模式 = screen
- 绘制 绿 色方块
- 绘制 蓝 色方块
<div class=sample>
<canvas class=s3-1 height=180 width=200></canvas>
<h4>canvas 2d 全局复合</h4>
<script>
!function () {
var canvas = document.querySelector(".s3-1"),
cc = canvas.getContext("2d");
cc.fillStyle = "red";
cc.fillRect(50, 20, 100, 100);
cc.globalCompositeOperation = "screen";
cc.fillStyle = "lime";
cc.fillRect(30, 40, 100, 100);
cc.fillStyle = "blue";
cc.fillRect(70, 60, 100, 100);
}();
</script>
</div>
方法 2. CanvasRenderingContext2D.prototype.putImageData
- 绘制 红 色方块
- 选定一个部分重叠的方块,使用
CanvasRenderingContext2D.prototype.getImageData
把该部分像素读入内存,对每个像素,和 绿 色应用 min(Cs+Cb,1) ,然后写入画布 - 选定一个部分重叠的方块,读取,每个像素和 蓝 色应用 min(Cs+Cb,1) ,写入画布
因为红绿蓝两两混合时每个分量必然一个是 0 一个是 1,所以并没有调用 Math.min 而是直接把分量置 1,或者说置 255。
<div class=sample>
<canvas class=s3-2 height=180 width=200></canvas>
<h4>在 canvas 中把颜色分量置 1,未使用 min(x + y, 1)</h4>
<script>
!function () {
var canvas = document.querySelector(".s3-2"),
cc = canvas.getContext("2d"),
i, len, idata, arr;
cc.fillStyle = "red";
cc.fillRect(50, 20, 100, 100);
for (i = 0,
idata = cc.getImageData(30, 40, 100, 100),
arr = idata.data,
len = arr.length; i < len; i += 4)
arr[i + 1] = arr[i + 3] = 255;
cc.putImageData(idata, 30, 40);
for (i = 0,
idata = cc.getImageData(70, 60, 100, 100),
arr = idata.data,
len = arr.length; i < len; i += 4)
arr[i + 2] = arr[i + 3] = 255;
cc.putImageData(idata, 70, 60);
}();
</script>
</div>
示例 - canvas webgl
webgl api - https://msdn.microsoft.com/en-us/library/dn621085(v=vs.85).aspx
webgl methods - https://msdn.microsoft.com/en-us/library/dn302341(v=vs.85).aspx
glBlendFunc + glBlendEquation 效果演示 - http://www.andersriggelsen.dk/glblendfunc.php
本文假设你对 webgl 一无所知。
本节的目标是在阅读本节内容之后,对 webgl 一无所知的读者能掌握 webgl 的基本思路、写出基本的 webgl 程序。如果不是这个情况,请跟帖指出,我会修正本节内容直至达到前述目标。
绘制方块
设 var gl = theCanvas.getContext("webgl");
,webgl 通过下面两个函数之一进行绘制,
gl.drawArrays(mode, first, count);
gl.drawElements(mode, count, type, offset);
这两个函数差不多,先解释 gl.drawArrays
。在调用 gl.drawArrays
之前 gl
必须满足下列条件,
- 使用
gl.useProgram
绑定了 1 个着色器程序 - 使用
gl.bindBuffer
绑定了 1 个数组,数组里面有内容可用 - 使用
gl.enableVertexAttribArray
启用了至少 1 个在顶点着色器里定义的特性 - 使用
gl.vertexAttribPointer
描述了启用的特性
上面 4 个条件就是发起 1 次 gl.drawArrays
调用所需的代码 + 数据,代码用 opengl es
着色器语言 glsl
写成,数据在 javascript
代码里面提供,并调用 webgl 的 javascript api 建立 js 数据
到 glsl 代码
的联系。
1 个 webgl 程序可以有很多着色器程序,每个着色器程序一定有 1 个顶点着色器和 1 个片段着色器。每绘制 1 个点,webgl 都依次调用顶点着色器和片段着色器。顶点着色器的唯一任务是给全局变量
gl_Position
赋值,它代表 1 个点的位置;片段着色器的唯一任务是给全局变量gl_FragColor
赋值,它代表刚才那个点的颜色。
gl.drawArrays
依次执行下列步骤,
- webgl 一次从数组读取由
gl.vertexAttribPointer
的stride
参数指出的字节,这些字节视为 1 个顶点 - 把这么多字节按
gl.vertexAttribPointer
指出的方式拆分后分别赋值给顶点着色器里面用attribute
定义的变量,调用了几次gl.vertexAttribPointer
就要给几个变量赋值 - 进入顶点着色器的
main
函数,main
里面一般会使用刚才赋值过的特性 - 顶点着色器的
main
结束,进入片段着色器的main
函数 - 片段着色器的
main
结束,1 个顶点渲染完毕,从数组读取下一个顶点 - 重复上述过程,直至处理了由
count
参数指出的顶点数
webgl 实际上读取的是从 javascript 数组拷贝到显卡上的数组
gl.vertexAttribPointer(
index, - 特性在和 gl.ARRAY_BUFFER 绑定的缓冲区中的索引
size, - 1 | 2 | 3 | [4],每个特性有几个分量,比如 vec3 有 3 个分量
type, - gl.BYTE | gl.UNSIGNED_BYTE | gl.SHORT | gl.UNSIGNED_SHORT | [gl.FLOAT]
normalized, - true,转化到 [-1.0, 1.0]
stride, - [0, 255],默认 0,单位字节,必须是 type 的整数倍
offset - 默认 0,单位字节,必须是 type 的整数倍
)
读作:为了给顶点着色器里面定义的第 index
号特性赋值,从数组中取 stride
个字节作为一个顶点,从这个顶点的第 offset
个字节开始取 size
个 type
,每个 type
依次对应特性的一个分量。
假设在顶点着色器里定义了 2 个特性
attribute vec3 position;
attribute vec2 resolution;
void main() { gl_Position = ???; }
每次进入顶点着色器的时候都希望这俩变量被赋值,以便在顶点着色器的 main
里面使用它们。在 javascript 里面用一个 Float32Array
保存顶点,数组形如
[x0, y0, z0, w0, h0, x1, y1, z1, w1, h1, ...]
下面的调用
gl.vertexAttribPointer(idPosition, 3, gl.FLOAT, false, 5 * 4, 0);
gl.vertexAttribPointer(idResolution, 2, gl.FLOAT, false, 5 * 4, 3 * 4);
// 4 是 Float32Array 数组的元素 Float32 的字节数,对应 gl.FLOAT
// 5 是说一个顶点有 5 个 Float32,5 * 4 是这个顶点的字节数
// 第 2 个调用里面的 3 是说 resolution 从每个顶点的第 3 个 Float32 开始
//
// idPosition 和 idColor 是 gl.getAttribLocation 返回的一个整数,
// 代表顶点着色器里面的特性 position 和 resolution。position 和 resolution
// 是在顶点着色器里面定义的变量,不能直接在 javascript 里面用,需要通过
// gl.getAttribLocation 建立一个对应关系
//
// 如果给 resolution 的 offset 参数传 0 则 resolution 和 position 重叠,
// 这没有问题但是数值可能没有意义
让 webgl 这样取值
| 数组中每个顶点的长度是 5 * 4 = 20(stride)个字节
| |
x0, y0, z0, w0, h0, x1, y1, z1, w1, h1, ...
| | | |
| | | 从数组 arr 的第 3 * 4 = 12(offset)个字节开始取 2(size)个
| | | gl.FLOAT(type)组成一个 vec2(arr[3], arr[4]),把这个 vec2
| | | 赋值给顶点着色器特性 resolution
| |
| 从数组 arr 的第 0(offset)个字节开始取 3(size)个 gl.FLOAT(type)组成
| 一个 vec3(arr[0], arr[1], arr[2]),把这个 vec3 赋值给顶点着色器特性 position
每个顶点的画布分辨率
resolution
都一样,所以一般不这么传递,放在这里只是为了举例。
gl.drawArrays(mode, first, count);
的 mode
参数从 webgl 定义的枚举里面取值,分 3 种类型
gl.POINTS
,点。数组中每个顶点代表一个点gl.LINES
、gl.LINE_STRIP
、gl.LINE_LOOP
,直线段。数组中每个顶点代表直线的一个端点或者说顶点,顶点之间的点由 webgl 以线性插值的方式计算出来gl.TRIANGLES
、gl.TRIANGLE_STRIP
、gl.TRIANGLE_FAN
,平面三角形。数组中每个顶点代表三角形的一个顶点,顶点之间的点由 webgl 以线性插值的方式计算出来
为了绘制一个方块,调用 gl.drawArrays(gl.TRIANGLE_FAN, offset, 4);
,意思是从当前绑定的数组的第 offset
个顶点开始用连续的 4 个顶点组成 1 个三角扇,这 4 个点的位置是事先规划好的,排列如下
0 3
1 2
webgl.1
- 4 个点的三角扇包含 2 个三角形,分别是
0 - 1 - 2
和0 - 2 - 3
,三角扇绘制的三角形的第 1 个顶点总是offset
处的那个顶点 - 这 2 个三角形共享 1 条边
0 - 2
,两条边的方向相反,第 1 个是2 -> 0
,第 2 个是0 -> 2
,说这样的 2 个三角具有相同的朝向。三角扇两个相邻三角形的朝向一定相同 - 如果改变了顶点的顺序,得到的三角扇可能就不是一个方块
有了这些知识下面写一个绘制黑色方块的程序,里面出现了顶点着色器和片段着色器代码,
- ie 只支持
experimental-webgl
- 这里面调用的函数
glProgram
在正式示例中定义,如果要运行需拷贝glProgram
函数
上面是注意事项
<canvas class=s4-rect width=200 height=180></canvas>
<script>
!function () {
var canvas = document.querySelector(".s4-rect"),
cc = canvas.getContext("webgl"),
// 片段着色器源代码
// 片段着色器必须用 precision mediump float 指出 float 的默认精度。
// 不像顶点着色器,片段着色器里面的 float 没有默认精度,不指定 float 默认
// 精度的话编译片段着色器就会失败。这是个比较荒唐的事实
// http://stackoverflow.com/questions/28540290/why-it-is-necessary-to-set-precision-for-the-fragment-shader
//
// 这个片段着色器代码就一句话,把所有顶点的颜色设置成不透明黑。由于是 4 个
// 顶点组成的方块,方块上除了 4 个顶点之外的点的颜色都由 webgl 通过线性插值
// 得出,不会进入片段着色器的 main,插值的结果还是不透明黑
sfs = "precision mediump float; void main() { gl_FragColor = vec4(0, 0, 0, 1); }",
// 顶点着色器源代码
// vec2 是两个 float。每处理一个顶点,从数组中取得的两个 float 都会赋值给
// 这个 attribute vec2 posxy,posxy 把数组传进来的内容原封不动地作为
// gl_Position 的 x 和 y
//
// gl_Position 的 4 个坐标 x, y, z, w 都是 [-1, 1] 的小数
//
// 绘制了 4 个顶点,所以顶点着色器的 main 总共进入 4 次。方块上其它点的坐标
// 由线性插值生成
svs = "attribute vec2 posxy; void main() { gl_Position = vec4(posxy, 0, 1); }",
// glProgram 的定义在下面的示例代码中给出
program = glProgram(cc, sfs, svs),
// 顶点数组。一会要通过 gl.bufferData 给顶点数组写入内容
arr = cc.createBuffer(),
i;
// gl.drawArrays 先决条件:gl.useProgram
cc.useProgram(program);
// gl.drawArrays 先决条件:gl.bindBuffer
cc.bindBuffer(cc.ARRAY_BUFFER, arr);
// 通过 gl.bufferData 往 arr 写入内容,作为顶点位置
// 参数里面并没有出现 arr,之所以能写进去是因为前面用 bindBuffer 指出了 arr 是
// 当前的 gl.ARRAY_BUFFER
// 最后一个参数是 gl.STATIC_DRAW,不用考虑
// 对照图 webgl.1
cc.bufferData(cc.ARRAY_BUFFER, new Float32Array([
-0.3, +0.3, // 0 - 左上
-0.3, -0.7, // 1 - 左下
+0.7, -0.7, // 2 - 右下
+0.7, +0.3 // 3 - 右上
]), cc.STATIC_DRAW);
// gl.drawArrays 先决条件:gl.enableVertexAttribArray
// 用 gl.getAttribLocation(program, "posxy") 获取顶点着色器里面定义的特性
// posxy 对应的整数索引,保存这个整数索引供 gl.enableVertexAttribArray 使用
i = cc.getAttribLocation(program, "posxy");
cc.enableVertexAttribArray(i);
// gl.drawArrays 先决条件:gl.vertexAttribPointer
// 一次从数组里取出 stride 个字节。如果只调用了一次
// gl.vertexAttribPointer,stride 也可以填 0,会自动计算 stride
cc.vertexAttribPointer(i, 2, cc.FLOAT, false, 2 * 4, 0);
// gl.drawArrays
cc.drawArrays(cc.TRIANGLE_FAN, 0, 4);
}();
</script>
上面是 gl.drawArrays
,它从用 gl.bindBuffer(gl.ARRAY_BUFFER, arr)
绑定的 arr
中依次读取每个顶点。gl.drawElements
需要用 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ids)
额外绑定一个数组作为 gl.ARRAY_BUFFER
的索引,这样一来它使用两个数组,arr
保存顶点,ids
保存遍历 arr
的顺序。设
// vertex 0, vertex 1, vertex 2, vertex 3, ...
arr = [ x0, y0, x1, y1, x2, y2, x3, y3, ...]
ids = [0, 3, 1, 2]
并且
- 已经调用了两次
gl.bindBuffer
让gl.ARRAY_BUFFER
和gl.ELEMENT_ARRAY_BUFFER
分别对应
arr
和ids
gl.vertexAttribPointer
指出每个顶点是 2 个type
- 索引数组
ids
的元素类型是Uint16
或者说gl.UNSIGNED_SHORT
则
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
绘制 4 次顶点,依次是0 - 1 - 2 - 3
gl.drawElements(gl.TRIANGLE_FAN, 4, gl.UNSIGNED_SHORT, 0)
绘制 4 次顶点,它依次读取ids
的每个元素,以元素的值作为arr
的索引去获取顶点,依次绘制arr
的0 - 3 - 1 - 2
我假设读者通过前面的阅读和练习已经理解了 gl.drawArrays
和 gl.drawElements
,下面简要介绍设置颜色。
前面在片段着色器里硬编码了个颜色,不透明黑,要画 3 个颜色的方块就需要 3 个片段着色器。如果能让片段着色器接受一个 javascript 传入的变量,有点像顶点着色器里面的特性 attribute
,从 javascript 指定颜色,那就可以只写一个片段着色器。出于两个原因,不使用 attribute
- 只有顶点着色器可以定义
attribute
,片段着色器不可以 - 方块是单色的,不需要像
attribute
那样每个顶点都传一个值
着色器程序总共可以定义 3 种变量:
attribute
、uniform
、varying
这里使用 uniform
。varying
用来从顶点着色器往片段着色器传值,当然也可以实现效果。
uniform
的意思是,每次调用 gl.drawArrays
绘制一系列的顶点之前,先设置一个在绘制过程中保持不变的值,绘制这些点的过程中,着色器程序可以读取但不能修改该定值。对比 attribute
,attribute
对 gl.drawArrays
绘制的每 1 个顶点都赋值 1 次;uniform
只在 gl.drawArrays
开始前赋值一次。
因为 uniform
在 1 次绘制中只赋值 1 次,所以它不从数组里面取值,gl.uniformXxx
用于设置 uniform
的值。
所以下面的示例中,顶点着色器定义 1 个 attribute
以接受顶点,片段着色器定义 1 个 uniform
以接受颜色,调用 3 次 gl.drawArrays
以绘制 3 个方块。
混合颜色
这里需要把 C=B(Cb,Cs) 换个形式以反映 webgl 的混合方法,换成 C=e(f(Cs),g(Cb)) 。看上去更复杂了,但马上就会发现,它很简单。
e
对应 gl.blendEquation(mode)
,mode
是 3 个枚举值之一
mode | gl.FUNC_ADD - 默认值
e(x,y)=clamp(x+y)
gl.FUNC_SUBTRACT
e(x,y)=clamp(x−y)
gl.FUNC_REVERSE_SUBTRACT
e(x,y)=clamp(y−x)
f
和
C=e(f(Cs),g(Cb))=clamp(Cs×1+Cb×1)=clamp(Cs+Cb)
当 Cs 和 Cb 都是正数时 clamp(Cs+Cb)=min(Cs+Cb,1) ,就是前面用过的 add 混合模式。相应的 js 代码是
而如果让
C=e(f(Cs),g(Cb))=clamp(Cs×(1−Cb)+Cb)=clamp(Cs+Cb−Cs×Cb)
当 Cs 和 Cb 都是正数时 clamp(Cs+Cb−Cs×Cb)=Cs+Cb−Cs×Cb ,就是前面用过的 screen 混合模式。相应的 js 代码是
代码现在的情况是
下面看具体的代码
要点
全部代码
参考复合与混合级别 1 svg 1.1 滤镜 滤镜效果模块级别 1 svg 复合 混合模式 欢迎使用Markdown编辑器写博客本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦:
快捷键
Markdown及扩展
使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接等,详细语法参考帮助?。 本编辑器支持 Markdown Extra , 扩展了很多好用的功能。具体请参考Github. 表格Markdown Extra 表格语法:
可以使用冒号来定义对齐方式:
定义列表
代码块代码块语法遵循标准markdown代码,例如:
脚注生成一个脚注1. 目录用 数学公式使用MathJax渲染LaTex 数学公式,详见math.stackexchange.com.
x=−b±b2−4ac−−−−−−−√2a
更多LaTex语法请参考 这儿. UML 图:可以渲染序列图: 或者流程图: 离线写博客即使用户在没有网络的情况下,也可以通过本编辑器离线写博客(直接在曾经使用过的浏览器中输入write.blog.csdn.net/mdeditor即可。Markdown编辑器使用浏览器离线存储将内容保存在本地。 用户写博客的过程中,内容实时保存在浏览器缓存中,在用户关闭浏览器或者其它异常情况下,内容不会丢失。用户再次打开浏览器时,会显示上次用户正在编辑的没有发表的内容。 博客发表后,本地缓存将被删除。 用户可以选择 把正在写的博客保存到服务器草稿箱,即使换浏览器或者清除缓存,内容也不会丢失。
浏览器兼容
08-23
评论
被折叠的 条评论
为什么被折叠?
到【灌水乐园】发言
查看更多评论
添加红包
|
---|