File: transparency.Rmd

package info (click to toggle)
rgl 1.3.34-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 13,968 kB
  • sloc: cpp: 23,234; ansic: 7,462; javascript: 6,125; sh: 3,555; makefile: 2
file content (201 lines) | stat: -rw-r--r-- 6,688 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
---
title: "A Note on Transparency"
author: "Duncan Murdoch"
date: "24/10/2020"
output:
  rmarkdown::html_vignette:
    fig_height: 5
    fig_width: 5
    toc: true
  pdf_document:
    fig_height: 5
    fig_width: 5
    toc: true
  html_document:
    default
vignette: >
  %\VignetteIndexEntry{A Note on Transparency}
  %\VignetteEngine{knitr::rmarkdown}
---

```{r setup, include=FALSE}
if (!requireNamespace("rmarkdown", quietly = TRUE) ||
    !rmarkdown::pandoc_available("1.14")) {
  warning(call. = FALSE, "These vignettes assume rmarkdown and Pandoc
          version 1.14.  These were not found. Older versions will not work.")
  knitr::knit_exit()
}
knitr::opts_chunk$set(echo = TRUE, snapshot = FALSE, screenshot.force = FALSE)
library(rgl)
options(rgl.useNULL = TRUE)
setupKnitr(autoprint = TRUE)

M <- structure(c(0.997410774230957, 0.0707177817821503, -0.0130676832050085, 
0, -0.0703366547822952, 0.99714070558548, 0.02762770652771, 0, 
0.0149840852245688, -0.0266370177268982, 0.999532878398895, 0, 
0, 0, 0, 1), .Dim = c(4L, 4L))
```

## Introduction

When drawing transparent surfaces, `rgl` tries to sort objects
from back to front to get better rendering of transparency.
However, it doesn't sort each pixel separately, so some pixels
end up drawn in the incorrect order.  This note
describes the consequences of that error, and 
suggests remedies.

## Correct Drawing

We'll assume that the standard `glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)` blending is used.  That is,
when drawing with transparency $\alpha$, the new colour is
mixed at proportion $\alpha$ with the colour previously
drawn (which gets weight $1-\alpha$.)

This note is concerned with what happens when two transparent
objects are drawn at the same location.  We suppose the further
one has transparency $\alpha_1$, and the closer one 
has transparency $\alpha_2$.  If they are drawn in the correct
order (further first), the three colours (background, further
object, closer object) should end up mixed in proportions
$C = [(1-\alpha_1)(1-\alpha_2), \alpha_1(1-\alpha_2), \alpha_2]$
respectively.

## Incorrect sorting

If the objects are drawn in the wrong order, the actual 
proportions of each colour will be 
$N = [(1-\alpha_1)(1-\alpha_2),\alpha_1, \alpha_2(1-\alpha_1)]$ 
if no masking occurs. 

`rgl` currently defaults to depth masking using `glDepthMask(GL_TRUE)`.  This means that depths are saved 
when the objects are drawn, and if an attempt is made to 
draw the further object after the closer one (i.e. as here),
the further object will be culled, and the proportions will be
$M = [(1-\alpha_2), 0, \alpha_2]$.

## Mask or Not?

The question is:  which is better, `glDepthMask(GL_TRUE)` or
`glDepthMask(GL_FALSE)`?  One way to measure this is to 
measure the distance between $C$ and the incorrect proportions.
(This is unlikely to match perceptual distance, which will
depend on the colours as well, but we need something. Some
qualitative comments below.)

So we have 

$$|C-N|^2 = 2\alpha_1^2\alpha_2^2,$$

and 

$$|C-M|^2 = 2\alpha_1^2(1-\alpha_2)^2.$$

Thus the error is larger with $N$ when $\alpha_2 > 1 / 2$, and 
larger with $M$ when $\alpha_2 < 1 / 2$.  The value of $\alpha_1$
doesn't affect the preference, though small values of $\alpha_1$ will be associated with smaller errors.  

Depending on the colours of the background and the two
objects, this recommendation could be modified.  For example,
if the two objects are the same colour (or very close),
it doesn't really matter how the 2nd and 3rd proportions
are divided up, and $N$ will be best because it gets the
background proportion exactly right.


## Recommendation

Typically in `rgl` we don't know which object will be closer 
and which one will be further, so we can't base our choice on
a single $\alpha_i$.  The recommendation would be to use
all small levels of `alpha` and disable masking, or use all
large values of `alpha` and retain masking.

## Example

The classic example of an impossible to sort scene involves
three triangles arranged cyclicly so each one is behind one and in front
of one of the others (based on https://paroj.github.io/gltut/Positioning/Tut05%20Overlap%20and%20Depth%20Buffering.html).

```{r}
theta <- 2*pi*c(0:2, 4:6, 8:10)/12
x <- cos(theta)
y <- sin(theta)
z <- rep(c(0,0,1), 3)
xyz <- cbind(x, y, z)
xyz <- xyz[c(1,2,6, 4,5,9, 7,8,3),]
open3d()
par3d(userMatrix = M)
triangles3d(xyz, col = rep(c("red", "green", "blue"), each = 3))
```

To see the effect of the calculations above, consider the following four displays.

```{r fig.width=8}
open3d()
par3d(userMatrix = M)
layout3d(matrix(1:9, ncol = 3, byrow=TRUE),
         widths = c(1,2,2), heights = c(1, 3,3), 
         sharedMouse = TRUE)
text3d(0,0,0, " ")
next3d()
text3d(0,0,0, "depth_mask = TRUE")
next3d()
text3d(0,0,0, "depth_mask = FALSE")
next3d()
text3d(0,0,0, "alpha = 0.7")
next3d()
triangles3d(xyz, col = rep(c("red", "green", "blue"), each = 3), alpha = 0.7, depth_mask = TRUE)
next3d()
triangles3d(xyz, col = rep(c("red", "green", "blue"), each = 3), alpha = 0.7, depth_mask = FALSE)
next3d()
text3d(0,0,0, "alpha = 0.3")
next3d()
triangles3d(xyz, col = rep(c("red", "green", "blue"), each = 3), alpha = 0.3, depth_mask = TRUE)
next3d()
triangles3d(xyz, col = rep(c("red", "green", "blue"), each = 3), alpha = 0.3, depth_mask = FALSE)
```

As you rotate the figures, you can see imperfections in
rendering.  On the right, the last drawn appears to be on top,
while on the left, the first drawn appears more opaque than
it should.  

In the figure below, the three triangles each have different transparency, and each use the recommended setting:

```{r}
open3d()
par3d(userMatrix = M)
triangles3d(xyz[1:3,], col = "red", alpha = 0.3, depth_mask = FALSE)
triangles3d(xyz[4:6,], col = "green", alpha = 0.7, depth_mask = TRUE)
triangles3d(xyz[7:9,], col = "blue", depth_mask = TRUE)
```

In this figure, all three triangles are the same colour,
only lighting affects the display:
```{r fig.width=8}
open3d()
par3d(userMatrix = M)
layout3d(matrix(1:9, ncol = 3, byrow=TRUE),
         widths = c(1,2,2), heights = c(1, 3,3), 
         sharedMouse = TRUE)
text3d(0,0,0, " ")
next3d()
text3d(0,0,0, "depth_mask = TRUE")
next3d()
text3d(0,0,0, "depth_mask = FALSE")
next3d()
text3d(0,0,0, "alpha = 0.7")
next3d()
triangles3d(xyz, col = "red", alpha = 0.7, depth_mask = TRUE)
next3d()
triangles3d(xyz, col = "red", alpha = 0.7, depth_mask = FALSE)
next3d()
text3d(0,0,0, "alpha = 0.3")
next3d()
triangles3d(xyz, col = "red", alpha = 0.3, depth_mask = TRUE)
next3d()
triangles3d(xyz, col = "red", alpha = 0.3, depth_mask = FALSE)
```

Here `depth_mask = FALSE` seems to be the right choice in both cases.