问题 什么是有效实现图像渲染的纯功能数据结构?


图像和像素渲染是Haskell最后的事情之一我无法选择一个足够有效的纯功能数据结构。为简单起见,我们来谈谈 1D 图像,因为那些可以很容易地扩展到n-d图像。我使用未装箱的矢量作为表示,并使用它们的可变视图进行渲染:

-- An 1D image is an unboxed vector of Pixels
type Position = Int
data Image    = Image { size :: Position, buffer :: UV.Vector Pixel }

-- A sprite is a recipe to draw something to an Image
newtype Sprite = Sprite (forall s .
    -> (Position -> Pixel -> ST s ()) -- setPixel
    -> ST s ())                       -- ST action that renders to Image

-- Things that can be rendered to screen provide a sprite
class Renderable a where
    getSprite a :: a -> Sprite

-- `render` applies all sprites, mutably, in sequence. `O(P)` (where P = pixels to draw)
render :: [Sprite] -> Image -> Image
render draws (Image size buffer) = Image size $ runST $ do ...

这是我发现的CPU渲染效率最高的设计,但它相当难看。对于实现的纯功能结构 render,显而易见的答案是使用地图来表示图像和列表 (Position, Pixel) 用来表示精灵。就像是:

-- 1D for simplicity
type Position = Int

-- An image is a map from positions to colors
type Image    = Map Position Pixel

-- A sprite is, too, a map from positions to colors
type Sprite   = [(Position, Pixel)]

-- Rendering is just inserting each pixel in sequence
-- (Not tested.)
render :: [Sprite] -> Image -> Image
render sprites image = foldr renderSprite image sprites
    where renderSprite sprite image = foldr (uncurry insert) image sprites

这是 O(P * log(N)) (P =要渲染的像素,N =图像的大小)。它可能看起来像 log(N) 是不可避免的,但如果你仔细看, render 正在经历相同的路径 Image 几次(即,如果你插入位置0,然后在位置1,你一直向下运行到一片叶子,然后一直向上,然后一直向下到邻居叶子......)。这看起来像完全相同的模式 zippers例如,可以改善渐近线,这让我怀疑 render 可以改进。 是否存在允许实现的纯功能数据结构 render 比...更好 O(P*log N)

注意:通过“纯粹的功能”,我特别指的是仅使用Haskell的代数数据类型的结构,即没有原生基元,例如 Int 和 Array (尽管那些在技术上更纯粹地用作纯数据结构)。


7636
2017-10-05 12:52


起源

怎么样 Array 要么 UArray? - Bartek Banachewicz
这并没有真正回答你的问题,但无论如何我还是想问。在您的第一种方法中,Sprite相当于一个调用列表 Position -> Pixel -> ST s ()。不能改成 [(Position, Pixel)] 在保持相同性能的同时使其更好? - madjar
就我的技能而言,没有。问题是 Position -> Pixel -> ST () 总是使用恒定的空间。为了使用列表,你必须始终计算每个列表将被融合并编译为一个恒定的空间循环。也许它只是我,但即使使用简单的渲染模式我也无法让它执行相同的操作...... - MaiaVictor
你能给出一个如何使用你的精灵的完整例子吗?通常当我想到精灵时,我会想到比它们渲染的图像小的图像,它们也可以重新定位。但我看不出你如何使用你的ST版本。 - ErikR
怎么没有?例如,这个 Sprite 在位置只渲染2个像素 6, 7: twoPixels :: Sprite; twoPixels set = set 6 black >> set 7 red。纯粹的版本将是 twoPixels = [(6, black), (7, red)]。 - MaiaVictor


答案:


如果精灵中的位置可以是任意的(例如 [(0,x),(7,y),(5000,z)])如果你只允许使用有界分支因子的数据结构,你似乎不能希望做得比O(P log N)更好。

如果精灵中的位置是连续的,那么你可以使用a Seq (fingertree)支持对数切片和连接实现 render 在O(log N)时间。如果你的精灵由k个不相交的连续序列组成,那么你可以重复这个k次为O(k log N) render

但是,我认为除非你愿意放弃一个O的额外因子(一维中的精灵的边长),否则扩展到两个维度并不像你说的那么容易。也许有某种手指k-d树可以避免这种额外的因素。


6
2017-10-05 15:58





你可以使用 区别 打包你的 Map 在O(n + p)时间:

render sprites image
    = flip union image
    . toMapWith (\new old -> new)
    . concat
    $ sprites

3
2017-10-05 18:26



我认为这是在幕后使用Array或类似的。 - Reid Barton
@ReidBarton确实,它可以完成 Array “在场景前”,如果愿意,也可以使用相同的渐近:只需使用 (//) 代替 union 和 toMapWith。 - Daniel Wagner