1= OFX Programming Guide : Spatial Effects 2Author:Bruno Nicoletti 32014-10-17 4:toc: 5:data-uri: 6:source-highlighter: coderay 7 8This guide will introduce the spatial coordinate system used by OFX and will illustrate that with a 9simple circle drawing plugin. 10Its source can be found in the pass:[C++] 11file `Guide/Code/Example5/circle.cpp`. 12This plugin takes a clip and draws a circle over it. The colour, size and position of the circle 13are controlled by several parameters. 14 15== Spatial Coordinate Systems 16There are two main coordinate systems in OFX, these are the **pixel coordinate system** and the **canonical coordinate system**. 17I'll describe them both, but first a slight digression. 18 19=== Pixel Aspect Ratios 20Some display formats (for example standard definition PAL and NTSC) have non square pixels, which is quite annoying 21in my opinion. The https://en.wikipedia.org/wiki/Pixel_aspect_ratio[**pixel aspect ratio**] defines how non-square 22your pixel is. 23 24image::Pics/kinder720.jpg[ role = "thumb", align=center, title=Addressable Pixels In An PAL 16:9 Image] 25 26For example, a digital PAL 16:9 wide-screen image has 720 by 576 actual addressable pixels, however it has a pixel aspect 27ratio of ~1.42 footnote:[Yes, it can also be 1.46, depending on who you talk to. 28Today I'm picking 1.42 29to force an exact 16 by 9 aspect on a PAL's 720x576 pixels]. 30 31image::Pics/kinderCanonical.jpg[ role = "thumb", align=center, title=PAL 16:9 As It Appears On Your Telly] 32This means on the display device, each of those pixels 33are stretched horizontally by a factor of ~1.42. If it were square pixels, our displayed image would actually have 1024 34pixels. 35 36Looking at the two images above you can distinctly see the affect that the PAR has, the image appears 'squashed' when 37viewed as raw pixels, but these stretch out to look correct on our final display device. 38 39=== Render Scales 40Applications typically let a user generate low resolution proxy previews of their work, either to save time 41or space until they have finished. In OFX we call this applying a **render scale**, which has two values 42one for X and one for Y. 43 44If we were doing a half resolution proxy render of our PAL 16:9 project, we'd have a renderscale of (0.5,0.5), and 45360 by 288 addressable pixels in an image, with a PAR of 1.42. 46 47image::Pics/kinderProxy.jpg[ role = "thumb", align=center, title=Addressable Pixels In An PAL 16:9 Image at Half Proxy ] 48=== Coordinate Systems 49We call the coordinate system which allows us to index honest to goodness pixels in memory, the **pixel coordinate system**. 50It is usually represented 51with integers. For example the **bounds** of our image data are in pixel coordinates. 52 53Now expressing positions or sizes on the image plane in pixel coordinates is problematic, both because 54of pixel aspect ratios and render scales. The problem with sizes is that a circle drawn with a constant radius in pixel coordinates 55will not necessarily be circular on the final display device, it will be stretched by the pixel aspect ratio. Circles won't be round. 56 57A similar problem applies to render scales, when you say something is at a position, it has to be independent of the renderscale. 58 59To get around this, we have the **canonical coordinate system**. This is on an idealised image plane, where all pixels 60are square and we have no scales applied to any data. Canonical coordinates are typically represented by doubles. 61 62Back to our PAL 16:9 example. The canonical coordinate system for that image would always has x==0 at the left and x==1023 63at the right, circles are always appear to be round and the arithmetic is easy. We use the canonical coordinate system 64to express render scale and PAR invariant data. This is the coordinate system we express spatial parameters in. 65 66There was a third coordinate system, the **Normalised Coordinate System**, but in practice this proved to be problematic 67and has been deprecated. 68 69=== Mapping Between Coordinate Systems 70Obviously on render you will need to map parameters from Canonical Coordinates to the required Pixel Coordinates, or vice versa. That 71is fortunately very easy, you just need to do a bit of multiplying via the pixel aspect ratio and the renderscale. 72 73Now, given... 74 75 - the pixel aspect ratio as _PAR_, which is available on the `**kOfxImagePropPixelAspectRatio**` double property on images and clips, 76 - the renderscale, as _SX_ and _SY_ , which is passed with in `inArgs` to a variety of actions in the 2D double property `**kOfxImageEffectPropRenderScale**`, 77 78==== Mapping from Canonical Coordinates to Pixel Coordinate 79---- 80x' = x * SX/PAR; 81y' = y * SY; 82---- 83 84==== Mapping from Pixel Coordinate to Canonical Coordinates 85---- 86x' = x * PAR/SX; 87y' = y/SY; 88---- 89 90== Loading Our Plugin 91This plugin highlights the fact that the OFX API is really a way a plugin and a host can have a discussion so they 92can both figure out how they should operate. It allows plugins to modify their behaviour depending on what the host 93says it can do. 94 95Here is the source for the load action... 96 97[source, c++] 98.circle.cpp 99---- 100 //////////////////////////////////////////////////////////////////////////////// 101 // The first _action_ called after the binary is loaded (three boot strapper functions will be howeever) 102 OfxStatus LoadAction(void) 103 { 104 // fetch our three suites 105 FetchSuite(gPropertySuite, kOfxPropertySuite, 1); 106 FetchSuite(gImageEffectSuite, kOfxImageEffectSuite, 1); 107 FetchSuite(gParameterSuite, kOfxParameterSuite, 1); 108 109 int verSize = 0; 110 if(gPropertySuite->propGetDimension(gHost->host, kOfxPropAPIVersion, &verSize) == kOfxStatOK) { 111 verSize = verSize > 2 ? 2 : verSize; 112 gPropertySuite->propGetIntN(gHost->host, 113 kOfxPropAPIVersion, 114 2, 115 gAPIVersion); 116 } 117 118 // we only support 1.2 and above 119 if(gAPIVersion[0] == 1 && gAPIVersion[1] < 2) { 120 return kOfxStatFailed; 121 } 122 123 /// does the host support multi-resolution images 124 gPropertySuite->propGetInt(gHost->host, 125 kOfxImageEffectPropSupportsMultiResolution, 126 0, 127 &gHostSupportsMultiRes); 128 129 return kOfxStatOK; 130 } 131---- 132 133It fetches three suites then it checks to see if the **kOfxPropAPIVersion** property exists on the host, if it does it 134then checks that the version is at least "1.2", as we later rely on features only available in that version of the API. 135 136The next thing it does is to check that the host is supports multiple resolutions. This is short hand for saying that 137the host allows input and output clips to have different regions of definition, and images may be passed 138to the plugin that have differing bounds. This is also a property of the plugin descriptor, but we've left it at the default value, 139which is 'true', as our plugin does support multiple resolutions. 140 141We are checking for multiple resolution support to conditionally modify our plugin's behaviour in later actions. 142 143 144== Description 145Now, onto our plugin. The description action is pretty standard, as is the describe in context action. I'll just show you 146snippets of the interesting bits. 147 148Note, we are relying on a parameter type that is only available with the 1.2 version of OFX. Our plugin checks for this version 149of the API the host supports and will fail gracefully during the load action. 150 151[source, c++] 152.circle.cpp 153---- 154 // set the properties on the radius param 155 gParameterSuite->paramDefine(paramSet, 156 kOfxParamTypeDouble, 157 RADIUS_PARAM_NAME, 158 &radiusParamProps); 159 160 gPropertySuite->propSetString(radiusParamProps, 161 kOfxParamPropDoubleType, 162 0, 163 kOfxParamDoubleTypeX); 164 165 gPropertySuite->propSetString(radiusParamProps, 166 kOfxParamPropDefaultCoordinateSystem, 167 0, 168 kOfxParamCoordinatesNormalised); 169 170 gPropertySuite->propSetDouble(radiusParamProps, 171 kOfxParamPropDefault, 172 0, 173 0.25); 174 gPropertySuite->propSetDouble(radiusParamProps, 175 kOfxParamPropMin, 176 0, 177 0); 178 gPropertySuite->propSetDouble(radiusParamProps, 179 kOfxParamPropDisplayMin, 180 0, 181 0.0); 182 gPropertySuite->propSetDouble(radiusParamProps, 183 kOfxParamPropDisplayMax, 184 0, 185 2.0); 186 gPropertySuite->propSetString(radiusParamProps, 187 kOfxPropLabel, 188 0, 189 "Radius"); 190 gPropertySuite->propSetString(radiusParamProps, 191 kOfxParamPropHint, 192 0, 193 "The radius of the circle."); 194---- 195 196Here we are defining the parameter that controls the radius of our circle we will draw. It's a double param, and the type of double param is **kOfxParamDoubleTypeX**, 197footnote:[this double parameter type is only available API versions 1.2 or above] which 198says to the host, this represents a size in X in canonical coordinates. The host can display that however it like, but to the API, it needs to pass values back in canonical coordinates. 199 200The other thing we do is to set up the default value. Which is 0.25, which seems to be a mighty small circle, as is the display maximum value of 2.0. However, 201note the property **kOfxParamPropDefaultCoordinateSystem** being set to **kOfxParamCoordinatesNormalised**, this says that defaults/mins/maxes are being described 202relative to the project size. So our circle's radius will default to be a quarter of the nominal project size's x dimension. For a 1080 HD project, this would be a value of 480. 203 204[source, c++] 205.circle.cpp 206---- 207 // set the properties on the centre param 208 OfxPropertySetHandle centreParamProps; 209 static double centreDefault[] = {0.5, 0.5}; 210 211 gParameterSuite->paramDefine(paramSet, 212 kOfxParamTypeDouble2D, 213 CENTRE_PARAM_NAME, 214 ¢reParamProps); 215 216 gPropertySuite->propSetString(centreParamProps, 217 kOfxParamPropDoubleType, 218 0, 219 kOfxParamDoubleTypeXYAbsolute); 220 gPropertySuite->propSetString(centreParamProps, 221 kOfxParamPropDefaultCoordinateSystem, 222 0, 223 kOfxParamCoordinatesNormalised); 224 gPropertySuite->propSetDoubleN(centreParamProps, 225 kOfxParamPropDefault, 226 2, 227 centreDefault); 228 gPropertySuite->propSetString(centreParamProps, 229 kOfxPropLabel, 230 0, 231 "Centre"); 232 gPropertySuite->propSetString(centreParamProps, 233 kOfxParamPropHint, 234 0, 235 "The centre of the circle."); 236---- 237Here we are defining the parameter that controls the position of the centre of our circle. It's a 2D double parameter and we are telling the host that 238it represents an absolute position in the canonical coordinate system 239footnote:[this double parameter type is only available API versions 1.2 or above]. Some hosts will automatically add user interface handles for such parameters to 240let you simply drag such positions around. We are also setting the default values relative to the project size, and in this case (0.5, 0.5), it should 241appear in the centre of the final image. 242 243[source, c++] 244.circle.cpp 245---- 246 // set the properties on the colour param 247 OfxPropertySetHandle colourParamProps; 248 static double colourDefault[] = {1.0, 1.0, 1.0, 0.5}; 249 250 gParameterSuite->paramDefine(paramSet, 251 kOfxParamTypeRGBA, 252 COLOUR_PARAM_NAME, 253 &colourParamProps); 254 gPropertySuite->propSetDoubleN(colourParamProps, 255 kOfxParamPropDefault, 256 4, 257 colourDefault); 258 gPropertySuite->propSetString(colourParamProps, 259 kOfxPropLabel, 260 0, 261 "Colour"); 262 gPropertySuite->propSetString(centreParamProps, 263 kOfxParamPropHint, 264 0, 265 "The colour of the circle."); 266---- 267This is obvious, we are defining an RGBA parameter to control the colour and transparency of our circle. Colours are always normalised 0 to 1, so when 268you get and set the colour, you need to scale the values up to the nominal white point of your image, which is implicitly defined by the data type of 269the image. 270 271[source, c++] 272.circle.cpp 273---- 274 if(gHostSupportsMultiRes) { 275 OfxPropertySetHandle growRoDParamProps; 276 gParameterSuite->paramDefine(paramSet, 277 kOfxParamTypeBoolean, 278 GROW_ROD_PARAM_NAME, 279 &growRoDParamProps); 280 gPropertySuite->propSetInt(growRoDParamProps, 281 kOfxParamPropDefault, 282 0, 283 0); 284 gPropertySuite->propSetString(growRoDParamProps, 285 kOfxParamPropHint, 286 0, 287 "Whether to grow the output's Region of Definition to include the circle."); 288 gPropertySuite->propSetString(growRoDParamProps, 289 kOfxPropLabel, 290 0, 291 "Grow RoD"); 292 } 293---- 294Finally, we are conditionally defining a boolean parameter that controls whether our circle affects the region of definition 295of our output image. We only able to modify the region of definition if the host has an architecture that supports that 296behaviour, which we checked at load time where we set the **gHostSupportsMultiRes** global variable. 297 298== Get Region Of Definition Action 299What is this region of definition action? Easy, an effect and a clip have a region of definition (RoD). This is the 300maximum rectangle for which an effect or clip can produce pixels. You can ask for RoD of a clip via the `**clipGetRegionOfDefinition**` 301function in the image effect suite. The RoD is currently defined in canonical coordinates footnote:[we are debating whether to modifying 302that to be in pixel coordinates]. 303 304Note that the RoD is independent of the **bounds** of a image, an image's bounds may be less than, more than or equal to the RoD. It is up to 305host how or why it wants to manage the RoD differently. As noted above, some hosts don't have the ability to do any such thing. 306 307By default the RoD of the output is the union of all the RoDs from all the mandatory input clips. In our example, we want to 308be able to set the RoD to be the union of the input clip with the area the circle we are drawing. Whether we do that or not is controlled by 309the "growRoD" parameter which is conditionally defined in the describe in context action. 310 311To set the output rod, we need to trap the `**kOfxImageEffectActionGetRegionOfDefinition**` action. Our MainEntry function now has an 312extra conditional in there.... 313 314[source, c++] 315.circle.cpp 316---- 317 ... 318 else if(gHostSupportsMultiRes && strcmp(action, kOfxImageEffectActionGetRegionOfDefinition) == 0) { 319 returnStatus = GetRegionOfDefinitionAction(effect, inArgs, outArgs); 320 } 321 ... 322---- 323 324Note that we dont trap this on hosts that aren't multi-resolution, as by definition on those hosts RoDs are fixed. 325 326The code for the action itself is quite simple... 327 328.circle.cpp 329---- 330 // tells the host what region we are capable of filling 331 OfxStatus 332 GetRegionOfDefinitionAction( OfxImageEffectHandle effect, 333 OfxPropertySetHandle inArgs, 334 OfxPropertySetHandle outArgs) 335 { 336 // retrieve any instance data associated with this effect 337 MyInstanceData *myData = FetchInstanceData(effect); 338 339 OfxTime time; 340 gPropertySuite->propGetDouble(inArgs, kOfxPropTime, 0, &time); 341 342 int growingRoD; 343 gParameterSuite->paramGetValueAtTime(myData->growRoD, time, 344 &growingRoD); 345 346 // are we growing the RoD to include the circle? 347 if(not growingRoD) { 348 return kOfxStatReplyDefault; 349 } 350 else { 351 double radius = 0.0; 352 gParameterSuite->paramGetValueAtTime(myData->radiusParam, time, 353 &radius); 354 355 double centre[2]; 356 gParameterSuite->paramGetValueAtTime(myData->centreParam, time, 357 ¢re[0], 358 ¢re[1]); 359 360 // get the source rod 361 OfxRectD rod; 362 gImageEffectSuite->clipGetRegionOfDefinition(myData->sourceClip, time, &rod); 363 364 if(rod.x1 > centre[0] - radius) rod.x1 = centre[0] - radius; 365 if(rod.y1 > centre[1] - radius) rod.y1 = centre[1] - radius; 366 367 if(rod.x2 < centre[0] + radius) rod.x2 = centre[0] + radius; 368 if(rod.y2 < centre[1] + radius) rod.y2 = centre[1] + radius; 369 370 // set the rod in the out args 371 gPropertySuite->propSetDoubleN(outArgs, kOfxImageEffectPropRegionOfDefinition, 4, &rod.x1); 372 373 // and say we trapped the action and we are at the identity 374 return kOfxStatOK; 375 } 376 } 377---- 378 379We are being asked to calculate the RoD at a specific time, which means that RoDs are time varying in 380OFX. 381 382We check our "growRoD" parameter to see if we are going to actually modify the RoD. If we do, we find out, in canonical coordinates, 383where we are drawing our circle. We then fetch the region of definition and make a union of those two 384regions. We then set the **kOfxImageEffectPropRegionOfDefinition** return property on **outArgs** and say 385that we trapped the action. 386 387All fairly easy. 388 389== Is Identity Action 390Our identity checking action is fairly obvious, we check to see if our circle has a non zero radius, and to see if we are 391not growing the RoD and our circle is outside the RoD. 392 393== Rendering 394The action code is fairly boiler plate, it fetches parameter values and images from clips before calling the templated 395PixelProcessing function. Which is below... 396 397.circle.cpp 398---- 399 template <class T, int MAX> 400 void PixelProcessing(OfxImageEffectHandle instance, 401 Image &src, 402 Image &output, 403 double centre[2], 404 double radius, 405 double colour[4], 406 double renderScale[2], 407 OfxRectI renderWindow) 408 { // pixel aspect of our output 409 float PAR = output.pixelAspectRatio(); 410 411 T colourQuantised[4]; 412 for(int c = 0; c < 4; ++c) { 413 colourQuantised[c] = Clamp<T, MAX>(colour[c] * MAX); 414 } 415 416 // now do some processing 417 for(int y = renderWindow.y1; y < renderWindow.y2; y++) { 418 if(y % 20 == 0 && gImageEffectSuite->abort(instance)) break; 419 420 // get our y coord in canonical space 421 float yCanonical = (y + 0.5f)/renderScale[1]; 422 423 // how far are we from the centre in y, canonical 424 float dy = yCanonical - centre[1]; 425 426 // get the row start for the output image 427 T *dstPix = output.pixelAddress<T>(renderWindow.x1, y); 428 429 for(int x = renderWindow.x1; x < renderWindow.x2; x++) { 430 // get our x pixel coord in canonical space, 431 float xCanonical = (x + 0.5) * PAR/renderScale[0]; 432 433 // how far are we from the centre in x, canonical 434 float dx = xCanonical - centre[0]; 435 436 // distance to the centre of our circle, canonical 437 float d = sqrtf(dx * dx + dy * dy); 438 439 // this will hold the antialiased value 440 float alpha = colour[3]; 441 442 // Is the square of the distance to the centre 443 // less than the square of the radius? 444 if(d < radius) { 445 if(d > radius - 1) { 446 // we are within 1 pixel of the edge, modulate 447 // our alpha with an anti-aliasing value 448 alpha *= radius - d; 449 } 450 } 451 else { 452 // outside, so alpha is 0 453 alpha = 0; 454 } 455 456 // get the source pixel 457 const T *srcPix = src.pixelAddressWithFallback<T>(x, y); 458 459 // scale each component around that average 460 for(int c = 0; c < output.nComponents(); ++c) { 461 // use the mask to control how much original we should have 462 dstPix[c] = Blend(srcPix[c], colourQuantised[c], alpha); 463 } 464 dstPix += output.nComponents(); 465 } 466 } 467 } 468---- 469Please don't think I actually write production code as slow as this, I'm just making the whole thing as clear as possible 470in my example. 471 472The first thing we do is to scale the normalised value for our circle colour up to a quantised value based on our data type. So 473multiplying up by 255 for 8 bit data types, 65536 for 16bit ints and 1 for floats. 474 475To draw the circle we are transforming a pixel's position in pixel space into a canonical coordinate. We then calculate 476the distance to the centre of the circle, again in canonical coordinates. We use that distance to see if we are inside or out 477of the circle, with a bit of anti-aliasing thrown in. This gives us a normalised alpha value. 478 479Our output value is our source pixel blended with our circle colour based on the intensity of the calculated alpha. 480 481 482== Summary 483This example plugin has shown ... 484 485 - the two main OFX spatial coordinate systems, 486 - how to use the region of definition action, 487 - that the API is a negotiation between a host and a plugiun, 488 - mapping between coordinate systems for rendering. 489 490 491