灯光设计
1. 灯光设计考虑
- 前提:基于一维LED排布
- 低成本实现或修改设计以及产品定义的灯效
- 减轻LED灯珠颗数变化带来的改动
- 将灯光系统看做一个低分辨率的显示系统来实现
2. 灯光基本设计思路
2.1 如何定义灯光
灯光显示实际是一个将几何图形转化为像素值(将矢量图转化为位图)的过程:LED灯光充当一个显示屏的角色,而几何图形是显示屏上的布局。
其中包括几个重要过程:定义几何图形,定义LED显示屏,光栅化。
1. 定义几何图形
- 考虑到前提是基于一维LED矩阵,在一维空间中我们只能抽象出点和线这两种基本几何图形,而点的长度无限趋于0,不适合作为几何图形来描述我们需要表现的灯光状态。因此我们选择以线段这个基本几何图形来描述我们所需要的矢量图。
我们需要为线段定义几个重要的属性:
属性 | 说明 | ---|--- position 位置(number)| 位置属性用来定义这条线段在显示屏中的位置。| anchor 锚点(number)| 锚点[1]用来定义线段的原点。 | scale 比例(number)| 比例用来控制线段的实际长度。| opacity 透明度(number) | 亮度用来定义线段颜色的透明度。| size 长度 (number) | 长度用来定义线段的最大长度。| visible 可见性 (boolean) | 可见性用来定义线段是否可见。| vertices 图形点(table)| 图形点用来描述线段的颜色。| node_list 子节点(table)| 子节点用来记录子线段。|
2. 定义LED显示屏
LED显示屏需要定义以下这些属性:
| 属性 | 说明 | |
|---|---|---|
| ledNum 灯珠颗数(number) | 定义几何图形生效的屏幕长度。 | |
| portalSpace 是否回环(boolean) | 当线段超出LED显示屏,该参数决定线段是尾出头进头出尾进,还是直接忽略掉过界部分。 | |
| pixels 像素值(table) | 线段光栅化后得到的每个LED的像素值。 | |
| mixPixels 混合后像素值(table) | 多层像素值叠加之后混合得到的像素值 | |
| bgColor 背景色(table) | LED显示屏的默认底色。 | |
| offset 偏移量(table) | 该块LED显示屏相对于起始点的偏移。 |
3. 通过光栅化将线段转化为LED像素值(矢量图转化为位图)。
2.2 如何定义灯光变化
- 灯光变化实际上就是几何图形属性值变化的过程。一个灯光动画可能包含了几个属性值的变化,这几个属性值的变化可能是同时发生的也可能是按顺序发生的,因此我们定义了以下这些动作用来描述线段属性变化的动作Action
| Actions | 影响的属性 | 作用 |
|---|---|---|
| AnchorToAction | anchor | 改变线段原点 |
| ColorToAction | vertices | 改变线段颜色 |
| FadeToAction | opacity | 改变线段透明度 |
| MoveToAction | position | 改变线段绝对位置 |
| MoveByAction | position | 改变线段相对位置 |
| ScaleToAction | scale | 改变线段长度 |
| Sequence | 取决于传入的actions | 实现actions按顺序执行 |
| Spawn | 取决于传入的actions | 实现actions同时执行 |
3.++灯光示例++:
灯光代码的lua部分分为三个部分:
/robot/activation/light/ : 包括了实现不同灯光效果的所需要的基础组件以及灯光进程启动文件。
/robot/activation-cust/light.lua :根据不同设备编写实现不同的灯光接口。
/robot/activation/comp/light.lua:封装dbus接口供其他进程在需要灯光的时候调用灯光接口。
如果我们需要编写新的灯光接口,只需要在robot/activation-cust/light.lua中添加新的函数和node即可。然后就可以在其他进程需要调用该灯光的地方调用对应函数。当然也可以新建一个light.lua替换旧的light.lua
- 下面是示例(以新建一个light.lua替换旧的为例):
- 建议先将原文件改名备份
- 新建一个light.lua文件
- 首先需要引入vertex.lua文件和node.lua
local Vertex = require("light.comp.vertex") local Node = require ("light.comp.node") - 然后需要新建一个rootNode,这个rootNode会在/robot/activation/light/main.lua被获取。其后所有的node都会加到rootNode的node_list里面去,作为子节点,从而能被访问到。
local rootNode = Node:new("root", 0, 1, 1, 0, 13, 13, true, true) rootNode:addVertex(Vertex:new(0, 0, 0, 0, 0)) rootNode:drawBgColor()
第一行代码,新建了一个局部变量rootNode,通过node.lua中提供的方法(如下)构造了一个table。
function Node:new (tag, position, anchor, scale, opacity, size, ledNum, visible, portalSpace, offset, bgColor)
local self = {
tag = tag;
position = position;
anchor = anchor;
scale = scale;
opacity = opacity;
size = size;
visible = visible;
ledNum = ledNum;
portalSpace = portalSpace;
vertices = {};
pixels = {};
mixPixels = {};
action_list = {};
node_list = {};
state = "INIT";
hasNewData = true;
offset = offset or 0;
bgColor = bgColor or {0, 0, 0, 0};
timer = Timer.new();
}
setmetatable(self, Node)
return self
end
在这个new()函数中,定义了一些变量。因为rootNode一般是起到管理所有node的作用,所以一般不会对它赋予灯效,因此主要关心的参数是visible,visible参数决定了该node以及其子节点是否可见,当前我们定义为可见。
第二行代码,为rootNode添加了一个vertex定义它的颜色,因为rootNode实际为不可见(opacity一直为0),所以为它定义什么颜色也不太重要。
第三行代码,为rootNode[pixels]进行赋值,主要是为了避免首次取值时获取到nil。
function Node:drawBgColor(a ,r, g, b)
for i = 0, self.ledNum - 1 do
self.pixels[i * 4 + 1] = a or self.bgColor[1] or 0
self.pixels[i * 4 + 2] = r or self.bgColor[2] or 0
self.pixels[i * 4 + 3] = g or self.bgColor[3] or 0
self.pixels[i * 4 + 4] = b or self.bgColor[4] or 0
end
end
然后以mini的音量灯光为例,mini的音量灯光效果为以type-C口为起始点,按照音量大小的比例显示白色灯光,音量最小时显示一颗灯珠,音量最大时显示12颗灯珠。将设备上的一组LED看做一个低分辨率的显示屏,那么就可以将需要显示的灯光抽象为该显示屏上需要显示的图形。也就是说,音量灯光可以看做是一条长度可变的白色线段画在长度为12的黑色屏幕上。
然后我们新建一个node来描述一条线段,从而来定义音量灯光。
-- Node:new (tag, position, anchor, scale, opacity, size, ledNum, visible, portalSpace, offset, bgColor) local volumeNode = Node:new("volume", 0, 0, 0, 0, 12, 12, true, true) volumeNode:addVertex(Vertex:new(0, 255, 255, 255, 255)) volumeNode:drawBgColor() rootNode:addChildNode(volumeNode)
第1行代码,创建了一个局部变量volumeNode:
- 第1个参数定义了这条线段的名称,主要是为了更方便的显示日志
- 第2个参数定义了线段的起始点的位置为0
- 第3个参数定义了线段的锚点(原点)为0,结合position和anchor我们才能确定线段的实际位置在哪里,第5个参数定义了线段的透明度,范围为0~1
第4个参数和第6个参数共同决定线段的实际长度,第6个参数确定线段的最大长度,确认后就不再更改,第4个参数定义了线段的伸缩比例,范围为0~1,伸缩比例与线段最大长度相乘得到线段的实际长度,所以更改线段长度不是改变size属性,而是改变scale属性;
通过这几个参数已经可以确定一条线段的位置、长度
- 第7个参数定义了屏幕的长度
- 第8个参数定义了该屏幕以及其上的图案是否可见
- 第9个参数定义了当这条线段移动到超出屏幕范围时,线段遵从尾出头进和头进尾出的策略
第2行代码,为这条线段定义颜色。使用vertex.lua中的new方法创建了一个颜色点,Vertex:new(0, 255, 255, 255, 255)描述了一条线段0位置处,颜色为#FFFFFFFF。并且将其加入到volumeNode的图形点集合中。
function Vertex:new(coord, alpha, red, green, blue)
local self = {
coord = coord,
alpha = alpha,
red = red,
green = green,
blue = blue,
}
setmetatable(self, Vertex)
return self
end
第3行代码作用同为volumeNode[pixels]进行赋值,主要是为了避免首次取值时获取到nil。
第4行代码将volumeNode添加到rootNode的node_list中,那么我们就可以在访问rootNode时访问到volumeNode
线段定义好了之后,我们需要去定义线段的变化,当音量发生变化的时候,线段长度也会发生变化。当我们收到音量调用的时候,线段需要以对应的长度显示在屏幕上。这个变化过程有两个属性发生了变化:一个是opacity,也就是线段的透明度,透明度需要由0增大到1,这样才能显示出来,另一个改变的属性是scale,也就是线段的长度,与音量大小成正比。因此需要定义一个方法。
local function setVolume(value) if not value or type(value[1]) ~= "number" then printErr("volume is not a number") return end local volume = value[1] if volume < 0 then volume = 0 elseif volume > 1 then volume = 1 end local scale = ScaleToAction:new(volumeNode.scale, volume * 11 / 12 + 1 / 12, 0.4 * math.abs(volume * 11 / 12 + 1 / 12 - volumeNode.scale), 1) local fade = FadeToAction:new(volumeNode.opacity, 1.0, 0, 1) volumeNode:addAction(scale, true) volumeNode:addAction(fade, false) end在这个函数中,先将需要的变量也就是音量从value中取出来,然后定义两个属性变化:scale变化,ScaleToAction:new()一共四个参数,第1个参数是起始scale为当前volumeNode的scale,第2个参数是目标scale为根据音量计算得到的结果,第3个参数定义了整个变化的时间,第4个参数定义了这个变化只发生1次;fade定义了opacity的变化,FadeToAction:new()也是需要4个参数,起始的opacity,目标opacity,整个变化需要的时间,这个变化会发生几次。
为了能在/robot/activation/light/main.lua中访问到这个方法,需要再添加以下代码:
local light = {"light.version.1.0"} function light.method(method, value) print("-------------> " .. method) local f = light[method]; if ( f ) then f(value); else printErr("invalid method", method); end end light.setVolume = setVolume然后我们将这个light.lua文件推到设备/etc/activation/位置,输入systemctl restart light重启灯光进程。然后调节音量就会调用到setVolume方法,就会显示对应的灯光。