├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── SUPPORT.md
└── lib
├── Lib.Academy.ColorSpaces.ctl
├── Lib.Academy.DisplayEncoding.ctl
├── Lib.Academy.OutputTransform.ctl
├── Lib.Academy.Tonescale.ctl
└── Lib.Academy.Utilities.ctl
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | **Version 2.0 Developer Release 3 (Sept 2, 2024):**
2 | * Bugs
3 | * Change lower hull gamma approximation from a constant value to a value determined using the log of the peak luminance. For SDR outputs, the value reduces to the former value but this change minimimizes some clipping artifacts that could occur at edge values at higher luminance outputs.s
4 | * Enhancements / simplifications
5 | * Replace calculation method for Mnorm used in the chroma compress function and remove REACH_GAMUT_TABLE
6 | * Make clamp to peak luminance active on forward and inverse direction (previously only active in forward direction) - helps avoid errors on inverse when users send in values outside the invertible range
7 | * Add extra clamp to PQ and HLG (derived from PQ 1000) EOTF options to protect against rare reintroduction of negative values. Though data is clamped from [0-peakLuminance] in a prior step, occasionally very small negative components can be reintroduced from precision errors during the XYZ to display primary matrix. Therefore, negative values are clamped as an extra protective step to avoid NaN errors from negative values which are undefined in the base PQ encoding.
8 |
9 | **Version 2.0 Developer Release 2 (August 19, 2024):**
10 | * Bugs
11 | * Correct a mistyped value used in pre-calculation of r_hit in the tone scale function init
12 | * Add a clamp to range [0 - peakLuminance] and remove explicit clamp in inverse EOTF function
13 | * Enhancements / simplifications
14 | * Set upper limit clamp value in AP1 clamping step to a value equal to 3 stops (8x) the minimum value required to reach maximum output from the tone scale function
15 | * Update table generation and lookup code to assure that hue values falling in the "wrap-around" hue region are handled correctly
16 | * Add two extra table entries in gamut cusp and upper gamma hull approximation tables that are duplicates of the first and last entries
17 | * Update indexing and lookup functions to expect a baseIndex offset to maintain proper indexing where 360 entries are assumed
18 | * Add a bool to determine whether white scaling should be applied - This allows the user to control when the white is or isn't applied, and is clearer and more robust than relying on a mismatch in the white point chromaticities. For example, a DCDM P3D65 ODT already has white point fitting handled by a 48/52.37 scale factor, so wouldn't need that additional white scaling that would otherwise be applied because the encoding white and limiting white would differ.
19 | * Remove unused smoothJ value
20 | * Simply compressFuncParams and compressPowerP function
21 | * Unused values in the 4-element compressionFuncParams were removed, retaining the one value that is used and renaming it to compressionThreshold to better describe what it controls
22 | * The compressionFuncParams related to power equaled 1, so the compressPowerP function was simplified where many of the pow() functions could be solved out when power=1
23 | * Rename REACH_CUSP_TABLE as REACHM_TABLE, because it represents the M value for the reach gamut at limitJmax. Also reduce REACHM_TABLE to only 1 column (M) since M is the only value that needs pre-computation (i.e. J is constant and always equal to limitJmax and hue is uniformly spaced and corresponds to row index)
24 | * Refactor the white scaling step as a discrete operator before display encoding on forward (plus a clamp) and immediately after display decoding on inverse
25 | * Reorder the enum values for EOTF encoding/decoding to be less haphazard
26 | * Remove unused parameters from display encoding/decoding function
27 | * Remove unused functions for applying surround parameter - The implementation was unvalidated and would need to be changed anyway if implemented in a future release, so the inactive code and supporting functions were removed to avoid any confusion for implementers.
28 | * Other minor refactorings of code for improved readability
29 | * Other ACES repos
30 | * Output Transforms
31 | * Update all TransformIDs to be more verbosely defined from transform parameter settings
32 | * Change default list of transforms
33 | * Add:
34 | * HLG 1000 nit
35 | * DCDM 48 nit and 300 nit
36 | * Rec2020 500 nit
37 | * D60 limited / sim versions of all provided transforms, organized in a separate d60 directory
38 | * Remove:
39 | * Rec2020 100 nit
40 | * P3-DCI
41 | * Input and Color Space Conversion Transforms
42 | * Set name space in Panasonic IDT to Panasonic
43 | * Update Sony Venice TransformIDs to be more consistent
44 | * Add a few missing transforms to provide to/from conversion to provided inputs
45 | * Add "Unity" transform
46 |
47 |
48 |
49 | **Version 2.0 Developer Release 1 (April 19, 2024):**
50 | * Reorganization of code:
51 | * This repository (`aces-dev`) will be renamed (`aces-core`) and houses the main rendering algorithms for ACES.
52 | * Output Transforms moved to [aces-output](https://github.com/ampas/aces-output)
53 | * Input Transforms moved to [aces-input-and-colorspaces](https://github.com/ampas/aces-input-and-colorspaces)
54 | * Look Transforms moved to [aces-look](https://github.com/ampas/aces-look)
55 | * AMF schema and example files moved to [aces-amf](https://github.com/ampas/aces-amf)
56 | * Documentation tracked at [aces-docs](https://github.com/ampas/aces-docs) and published using mkdocs to [ACEScentral](docs.acescentral.com)
57 | * All TransformIDs have been conformed to the v2 TransformID specification
58 | * New core rendering algorithm:
59 | * New tonescale function with a lower default contrast and adaptability to produce output for any peak luminance between 100-10000 nits.
60 | * A hue-preserving rendering transform, applying luminance mapping mostly independent of color adjustments.
61 | * Gamut mapping to mostly avoid undesirable clipping but still allow for reaching the edges of the display gamut volume.
62 | * Invertibility up to at least P3 gamut
63 | * Reference images have been moved to be tied to their respective transform repositories
64 |
65 |
66 |
67 | **Version 1.3 (April 30, 2021):**
68 |
69 | * New Features:
70 | * Add gamut compression transform to assist with remapping problematic colorimetry into AP1
71 | * Update AMF schema to with refinements by the AMF Implementation Working Group:
72 | * Update all `aces:transformType`s to have choice of `aces:uuid`, `aces:file`, or `aces:transformId`
73 | * Modify `outputTransformType` to no longer be an extension of `transformType`
74 | * Update choice occurences to disallow the possibility of empty transform tags
75 | * Add CSC transforms for Sony Venice
76 | * Bug fixes:
77 | * Remove clamp in ACES to ACEScg conversion transform
78 | * Correct typos in the TransformIDs of CSC files added in v1.2
79 | * Other:
80 | * Relocate ACES documentation to its own repository
81 | * Rename `outputTransforms` directory to `outputTransform` (singular)
82 | * Add color primary subdirectories to `outputTransform` directory to be consistent with the `odt` directory
83 | * Update reference images:
84 | * Add images to accompany new gamut compression transform
85 | * Add images to accompany Sony Venice CSC transforms
86 | * Update images for ACES <--> ACEScg conversion transforms
87 |
88 | **Version 1.2 (April 1, 2020):**
89 |
90 | * New Features:
91 | * Add ACES Metadata File specification document (S-2019-001), XML schema, and example files
92 | * Add new version of Common LUT Format specification document (S-2014-006)
93 | * Add new ACES Project Organization and Development Procedure document (P-2019-001)
94 | * Add ACES Color Space Conversion transforms between:
95 | * ACES and Canon Canon Log 2 Cinema Gamut
96 | * ACES and Canon Canon Log 3 Cinema Gamut
97 | * ACES and ARRI ALEXA LogC (EI800) WideGamut
98 | * ACES and RED Log3G10 REDWideGamutRGB
99 | * ACES and Sony S-Log3 S-Gamut3
100 | * ACES and Sony S-Log3 S-Gamut3.Cine
101 | * ACES and Panasonic Varicam V-Log V-Gamut
102 | * Add HDR Output Transforms (RRT+ODT):
103 | * P3D65 (1000 cd/m^2) ST.2084 (and inverse)
104 | * P3D65 (2000 cd/m^2) ST.2084 (and inverse)
105 | * P3D65 (4000 cd/m^2) ST.2084 (and inverse)
106 | * Add vendor-supplied IDTs for Sony VENICE
107 | * Bug Fixes:
108 | * Add missing D65 to D60 CAT to 'InvODT.Academy.P3DCI_D65sim_48nits.ctl'
109 | * Other:
110 | * Revert function parameters of ODTs with full/legal option from 'uniform bool' to 'varying int'
111 | * Update ACES System Versioning document (S-2014-002)
112 | * Update TransformIDs of ACES reference implementation transforms
113 | * Remove "Academy Color Encoding System (ACES) Clip-level Metadata File Format Definition and Usage" (TB-2014-009)
114 | * Add reference images to accompany new ACEScsc transforms
115 | * Various minor typo fixes in document LaTeX source files
116 |
117 |
118 | **Version 1.1 (June 21, 2018):**
119 |
120 | * New Features:
121 | * Add P3 ODTs:
122 | * P3D65 (and inverse)
123 | * P3D65 "D60 simulation" (i.e. D60 adapted white point) (and inverse)
124 | * P3DCI "D65 simulation" (i.e. D65 adapted white point) (and inverse)
125 | * P3D65 limited to Rec.709 (inverse not required)
126 | * Add Rec.2020 ODTs:
127 | * Rec.2020 limited to Rec.709 (inverse not required)
128 | * Rec.2020 limited to P3D65 (inverse not required)
129 | * Add DCDM ODT:
130 | * DCDM with D65 adapted white point and limited to P3D65 (and inverse)
131 | * Add new ACESlib files:
132 | * SSTS: code for the Single Stage Tone Scale used in HDR Output Transforms
133 | * OutputTransforms: beginning of modules needed for parameterizing Output Transforms
134 | * Add new subfunctions to existing ACESlib files:
135 | * Utilities_Color:
136 | * functions for converting between PQ and HLG at 1000 nits
137 | * `_f3` versions of `moncurve` and `bt1886` encoding functions
138 | * ODT_Common:
139 | * `_f3` version of `Y_2_linCV` and `linCV_2_Y`
140 | * RRT_Common:
141 | * functions containing the non-tone scale portion of the RRT for simpler application in forward and inverse Output Transforms
142 | * Add new ACESutil functions:
143 | * DolbyPQ_to_HLG_1000nits
144 | * HLG_to_DolbyPQ_1000nits
145 | * Add HDR Output Transforms (RRT+ODT):
146 | * Rec.2020 (1000 cd/m^2) ST.2084 (and inverse)
147 | * Rec.2020 (2000 cd/m^2) ST.2084 (and inverse)
148 | * Rec.2020 (4000 cd/m^2) ST.2084 (and inverse)
149 | * Rec.2020 (1000 cd/m^2) HLG (and inverse)
150 | * Remove HDR ODTs (and inverses)
151 | * `ODT.Academy.P3D60_ST2084_1000nits.ctl`
152 | * `ODT.Academy.P3D60_ST2084_2000nits.ctl`
153 | * `ODT.Academy.P3D60_ST2084_4000nits.ctl`
154 | * `ODT.Academy.Rec2020_ST2084_1000nits.ctl`
155 | * `InvODT.Academy.P3D60_ST2084_1000nits.ctl`
156 | * `InvODT.Academy.P3D60_ST2084_2000nits.ctl`
157 | * `InvODT.Academy.P3D60_ST2084_4000nits.ctl`
158 | * `InvODT.Academy.Rec2020_ST2084_1000nits.ctl`
159 | * Rename some existing transforms for clarity:
160 | * Rename `DCDM_P3D60` to `DCDM_P3D60limited`
161 | * Rename `P3DCI` to `P3DCI_D60sim`
162 | * Rename `RGBmonitor` to `sRGB`
163 | * Add LMT that can help correct bright blue light clipping or hue shifts
164 | * Add new reference images for new transforms
165 | * Add documentation:
166 | * TB-2018-001 - Derivation of the ACES White Point CIE Chromaticity Coordinates
167 | * Python module with reference implementation of TB-2018-001
168 | * iPython Notebook with calculation of values used in TB-2018-001
169 | * Bug Fixes:
170 | * Arri IDT - Improve linearization of LogC data
171 | * Other:
172 | * Miscellaneous white space and line wrap fixes in CTL transforms
173 | * Miscellaneous typo fixes in CTL transform comments
174 | * Miscellaneous README and CTL comment updates
175 | * Miscellaneous LaTeX documentation typo and code fixes
176 | * Update ACEStransformIDs where appropriate
177 | * Update README and CHANGELOG
178 |
179 | **Version 1.0.3 (September 20, 2016):**
180 |
181 | * New Features:
182 | * Add new ACEScct color correction working space transforms
183 | * Add ACEScct specification document
184 | * Add Sony S-Log3 / S-Gamut3 IDTs
185 | * Add functions to convert between premultiplied and straight alpha
186 | * Add D65 RGB Monitor ODT
187 | * Add new reference images for new transforms
188 | * Bug Fixes:
189 | * Update copy and paste typo in ACESproxy document
190 | * Update ODT functions legal range input variable usage to avoid a situation where it may not execute as intended.
191 | * Update miscellaneous to local variables in utility functions to avoid clashes with existing global variables
192 | * Update miscellaneous minor errors in Transform IDs
193 | * Update miscellaneous transforms missing ACESuserName Tags
194 | * Other:
195 | * Update IDT READMEs to reflect latest manufacturer provided information including broken links
196 | * Restructure utility and lib functions directories for use clarification
197 | * Restructure directories to consolidate CSC transforms
198 | * Update equation variables names in ACEScc and ACESproxy documents for greater clarity
199 | * Miscellaneous math simplifications in utility functions
200 | * Miscellaneous white space fixes in CTL transforms
201 | * Miscellaneous typo fixes in CTL transform comments
202 | * Remove version number from CTL file names
203 | * Add Python script to rename CTL files based on TransformID
204 | * Update all documents to remove version numbers and use date as unique identifier
205 | * Update all documents to use vector logo
206 | * Update reference images to reflect code changes
207 | * Update README and CHANGELOG
208 |
209 | **Version 1.0.2 (April 12, 2016):**
210 |
211 | * Add Missing chromatic adaptation transform in Rec2020 1000-nit InvODT
212 | * Fix ACEStransformID references in DolbyPQ utility CTLs
213 | * Fix lingering ST2048 references
214 | * Fix typo in file names in images/README
215 | * Fix file names in images README
216 | * Various minor typographical and stylistic fixes
217 | * Rename all references to ST2084 from ST2048
218 | * Tabs => spaces and blank space cleanup
219 | * Update README and CHANGELOG
220 |
221 | **Version 1.0.1 (September 4, 2015):**
222 |
223 | * Add ACES technical documentation as LaTeX source to facilitate tracking of document revisions
224 | * Add utility functions for making the OCIO config
225 | * Bug fixes:
226 | * Fix "ACES to ACESproxy" transforms - Correct a bug in piecewise function logic for lin_2_acesProxy()
227 | * Improve readability and robustness of some transforms based on suggestions from users
228 | * Fix various typos in CTL transform comments and documentation
229 | * Fix a broken link to the reference images
230 | * Modify HDR ODT transforms
231 | * Adjust the lowest four coefficients for the HDR ODT tonescales in conjunction with a small offset to allow for obtaining a code value of 0
232 | * Add a Rec.2020 version of the 1000 nit ODT
233 | * Change HDR transforms references to "PQ" to "ST2084"
234 |
235 | **Version 1.0 (December 20, 2014):**
236 |
237 | * Additional transforms, encodings, documents, and reference images are included as part of the ACES Version 1.0 release. Please carefully review the ACES Version 1.0 documentation package for details on new features and enhancements for ACES Version 1.0
238 | * Update filenames have been to conform to the ACES System Versioning Specification
239 | * RRT
240 | * Add a new set of rendering primaries to improve gradeability and vectorscope behavior. The new primaries, known as AP1, are near the spectrum locus but exceed anticipated device gamuts, including ITU-R BT.2020 at a range of white points.
241 | * Modify the global desaturation to apply in RGB space prior to the RRT tone scale. This was done to improve the overall look of the images based on end-user feedback.
242 | * Modify The red modifier and glow module variables have been modified. This was done to improve the overall look of the images based on end-user feedback.
243 | * Add a clip of negative values prior to the application of the 3x3 matrix that converts ACES to the rendering primaries. This is added to avoid an error that can occur with negative and saturated ACES values turning positive and saturated.
244 | * Modify the RRT tone scale to address end-user concerns that the default rendering in v0.7.1 unnecessarily crushed shadow detail
245 | * Modify the output luminance of an 18% scene reflector from 5.0 nits to 4.8 nits to slightly darken the overall image in response to end-user feedback
246 | * Remove the hue restore function to improve grading behavior and address rare instances where image noise could be enhanced.
247 | * Modify the RRT tone scale to allow for the use of b-splines in the new HDR ODTs.
248 | * ODTs
249 | * Add a new set of rendering primaries to improve gradeability and vectorscope behavior. The new primaries, known as AP1, are near the spectrum locus but exceed anticipated device gamuts, including ITU-R BT.2020 at a range of white points.
250 | * Modify ODT tone scale to address end-user concerns that the default rendering in v0.7.1 unnecessarily crushed shadow detail
251 | * Remove hue restore and smart-clip functions to improve grading behavior and address rare instances where image noise could be enhanced
252 | * Modify ODT tone scales to allow the ability to achieve device black on-set and more quickly in the DI environment
253 | * Add Rec.709, Rec.2020, and rgbMonitor ODTs supporting dim surround environments
254 | * Add a runtime flag for full range or legal range output in Rec.709 ODTs. The default is full range.
255 | * Update ACEScc (formerly ACESlog) and ACESproxy transforms
256 | * Miscellaneous code cleanups. Removal of unused code
257 |
258 |
259 | **Version 0.7.1 (February 26, 2014):**
260 |
261 | * Bug fixes:
262 | * Correct the value for a constant in the F65-F55 10-bit IDTs
263 | * Correct the forward Rec2020 ODTs which were applying the inverse tonescale
264 |
265 | **Version 0.7 (January 31, 2014):**
266 |
267 | * Update RRT (forward and inverse):
268 | * Removal of rendering primaries to address a bug which can occur during exposure grading. Tone scale application is performed directly on ACES RGB.
269 | * Replacement of the red chroma scaling step with an alternate algorithm to modify reds. This algorithm darkens red colors instead of reducing chroma.
270 | * Addition of a "glow module", which provides a modest lightening and saturation boost in colored shadow regions.
271 | * Update ODTs (forwards and inverses):
272 | * Removal of rendering primaries to address a bug which can occur during exposure grading. Tone scale application is performed directly on OCES RGB.
273 | * Add new math in the ODT tone scale application to preserve more highlight saturation.
274 | * Add Device code value clamping at 1.0, followed by a hue-restore step.
275 | * Change in clipping behavior for the Rec709 and rgbMonitor ODTs to produce a closer visual approximation to image appearance on a digital cinema projector.
276 | * Refactor code to centralize functions common to more than one transform. This removes redundancy and makes the differences between various transforms easier to identify for implementers.
277 | * Update IDTs:
278 | * Update ARRI Alexa IDTs - Modify sensor-RGB-to-ACES matrices for nitpickers. ACES diff ~0.00x; LAB diff after RRT + rgbMonitor ODT ~0.0000x.
279 | * Update the Sony IDTs - Update F35 ODTs to use the piecewise S-Log1 function. Include F65/F55 IDTs for daylight and tungsten.
280 | * Fix a typo in ACES-to-ACESproxy10 transform.
281 |
282 | **Version 0.2.2 (October 15, 2013):**
283 |
284 | * Restore DCDM ODT with bug fixes
285 | * Add a variant of the DCDM ODT that limits X'Y'Z' values to P3D60 gamut
286 | * Relabel "dcsim" ODTs as "d60sim"
287 | * Modify highlight handling of "d60sim" ODTs to avoid chromaticity shifts in highlights
288 | * Refactor ODTs for consistency and improved readability
289 | * Fix a bug in RRT that could occur when ACES triplets had a negative valued mean
290 |
291 | **Version 0.2.1 (August 9, 2013):**
292 |
293 | * Temporarily remove DCDM ODT
294 | * Rename RGB monitor ODT transforms to clarify intended usage (dcsim)
295 |
296 | **Version 0.2 (August 2, 2013):**
297 |
298 | * Update RRT
299 | * Update ODTs
300 | * Add ODTs for display devices which conform to ITU-R BT.2020
301 | * Rename ODTs to better reflect their intended usage
302 | * Add Inverse RRT and ODT transforms
303 | * Add ACESlog and ACESproxy documentation, reference implementations, and images
304 | * Remove ctlrender - now part of CTL
305 | * Remove document binaries and replaced with links
306 | * New distribution links for golden reference images
307 |
308 | **Version 0.1.1 (June 25, 2012):**
309 |
310 | * Add dw ratio preserving ODT tone curve
311 | * Fix possible underflow conditions in RRT and ODT CTL transforms
312 | * Bug fix in ctlrender to avoid streaks in output images
313 | * Add reverse RRT and RDT splines
314 | * Modify RDT spline coefficients to tweak shadow reproduction
315 |
316 | **Version 0.1 (March 1, 2012):**
317 |
318 | * Add transforms including RRT
319 | * Add Test Images
320 | * Add ctlrender sample application for processing images through CTL transforms
321 | * Add ACES, ADX, and CTL documentation
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to ACES
2 |
3 | We're thrilled that you're interested in contributing to ACES. To maintain the legal integrity of the project's codebase, we require all contributors to sign a Contributor License Agreement (CLA).
4 |
5 | ## Signing the CLA
6 |
7 | - Before we can merge any of your contributions, you must sign our CLA.
8 | - The process is simple. When you submit a pull request for the first time, you will be prompted to sign the CLA online.
9 | - If you are contributing on behalf of your employer or if your contributions are owned by someone other than yourself (e.g., your employer), please make sure you have the right to submit the contributions under our project's CLA.
10 |
11 | By signing the CLA, you assure the project and its users that your contributions do not infringe upon the rights of any third parties and that the project can use your contributions without legal repercussions.
12 |
13 | If you have any questions about the CLA process, please feel free to contact a member of the ACES Team via ACESCentral.com.
14 |
15 | ## Requirement for Signed Commits
16 |
17 | As part of our commitment to security and the integrity of our codebase, we require all commits to be signed. This helps us ensure that contributions are actually made by the account they come from and not altered by a third party.
18 |
19 | ### Why Signed Commits?
20 |
21 | Signed commits provide an additional layer of security by guaranteeing that the commits are from a verified source. This is crucial for maintaining the trustworthiness of our codebase.
22 |
23 | ### How to Sign Commits
24 |
25 | To sign commits, you'll need to use a GPG (GNU Privacy Guard) or S/MIME (Secure/Multipurpose Internet Mail Extensions) key. If you haven't already set up a GPG key, you can follow [GitHub's guide on generating a new GPG key](https://docs.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key).
26 |
27 | Once you have a GPG key, you can add it to your GitHub account. For instructions on how to do this, see [GitHub's documentation on adding a new GPG key to your account](https://docs.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account).
28 |
29 | When you have your GPG key added to your GitHub account, you can start signing your commits. If you're using the command line, you can sign commits with `git commit -S -m "Your commit message"`.
30 |
31 | ### Verifying a Signed Commit
32 |
33 | You can verify that your commits are signed by looking for the "Verified" label on GitHub's commit interface.
34 |
35 | ### What if You Cannot Sign Your Commits?
36 |
37 | We understand that in some scenarios, you might not be able to sign commits. If you find yourself in this situation, please reach out to the project maintainers for assistance.
38 |
39 | For more detailed instructions on signing commits, you can refer to [GitHub's documentation on signing commits](https://docs.github.com/en/github/authenticating-to-github/signing-commits).
40 |
41 | ---
42 |
43 | We appreciate your contributions to ACES, and we thank you for adhering to our signed commits policy. This policy helps us maintain the security and integrity of our codebase.
44 |
45 | If you have any questions about this process, please feel free to contact the project maintainers.
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # License Terms for Academy Color Encoding System Components #
2 |
3 | Academy Color Encoding System (ACES) software and tools are provided by the
4 | Academy under the following terms and conditions: A worldwide, royalty-free,
5 | non-exclusive right to copy, modify, create derivatives, and use, in source and
6 | binary forms, is hereby granted, subject to acceptance of this license.
7 |
8 | Copyright © 2015 Academy of Motion Picture Arts and Sciences (A.M.P.A.S.).
9 | Portions contributed by others as indicated. All rights reserved.
10 |
11 | Performance of any of the aforementioned acts indicates acceptance to be bound
12 | by the following terms and conditions:
13 |
14 | * Copies of source code, in whole or in part, must retain the above copyright
15 | notice, this list of conditions and the Disclaimer of Warranty.
16 |
17 | * Use in binary form must retain the above copyright notice, this list of
18 | conditions and the Disclaimer of Warranty in the documentation and/or other
19 | materials provided with the distribution.
20 |
21 | * Nothing in this license shall be deemed to grant any rights to trademarks,
22 | copyrights, patents, trade secrets or any other intellectual property of
23 | A.M.P.A.S. or any contributors, except as expressly stated herein.
24 |
25 | * Neither the name "A.M.P.A.S." nor the name of any other contributors to this
26 | software may be used to endorse or promote products derivative of or based on
27 | this software without express prior written permission of A.M.P.A.S. or the
28 | contributors, as appropriate.
29 |
30 | This license shall be construed pursuant to the laws of the State of
31 | California, and any disputes related thereto shall be subject to the
32 | jurisdiction of the courts therein.
33 |
34 | Disclaimer of Warranty: THIS SOFTWARE IS PROVIDED BY A.M.P.A.S. AND CONTRIBUTORS
35 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
36 | THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
37 | NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL A.M.P.A.S., OR ANY
38 | CONTRIBUTORS OR DISTRIBUTORS, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
39 | SPECIAL, EXEMPLARY, RESITUTIONARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
40 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
41 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
42 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
43 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 |
46 | WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, THE ACADEMY SPECIFICALLY
47 | DISCLAIMS ANY REPRESENTATIONS OR WARRANTIES WHATSOEVER RELATED TO PATENT OR
48 | OTHER INTELLECTUAL PROPERTY RIGHTS IN THE ACADEMY COLOR ENCODING SYSTEM, OR
49 | APPLICATIONS THEREOF, HELD BY PARTIES OTHER THAN A.M.P.A.S.,WHETHER DISCLOSED OR
50 | UNDISCLOSED.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Academy Color Encoding System Core Transforms ##
2 |
3 | [](https://cla-assistant.io/ampas/aces-dev)
4 |
5 | The Academy Color Encoding System (ACES) is a set of components that facilitates a wide range of motion picture and television workflows while eliminating the ambiguity of legacy file formats. The system is designed to support both all-digital and hybrid film-digital motion picture workflows.
6 |
7 | In ACES 2.0, the basic ACES components are divided across a few individual repositories:
8 |
9 | * This repository (`aces-core`) houses the core transforms for ACES.
10 | * A set of preset Output Transforms to common outputs are tracked at [aces-output](https://github.com/ampas/aces-output), along with reference images that can be used to verify the output of each transform
11 | * Input Transforms are tracked at [aces-input-and-colorspaces](https://github.com/ampas/aces-input-and-colorspaces)
12 | * Look Transforms are collected in [aces-look](https://github.com/ampas/aces-look)
13 | * AMF schema and example files can be found at [aces-amf](https://github.com/ampas/aces-amf)
14 | * Documentation is written in markdown and tracked at [aces-docs](https://github.com/ampas/aces-docs). It is published using mkdocs to [ACEScentral](docs.acescentral.com).
15 |
16 | Regular snapshots of the entire system bundled, tagged and can be downloaded from [aces](https://github.com/ampas/aces).
17 |
18 | ## Previous Versions
19 | The full code history of the ACES pre-2.0 remains in the commit history of this repository and can be accessed by checking out the relevant branch and/or tagged commit to view the transforms at that previous version.
20 |
21 | Tagged versions of ACES can be browsed in the [tag history](https://github.com/ampas/aces-dev/tags), including [ACES version 1.3](https://github.com/ampas/aces-dev/releases/tag/v1.3)
22 |
23 | ## Prerequisites ##
24 |
25 | ### Color Transformation Language ###
26 |
27 | Color Transformation Language (CTL) can be downloaded from
28 | https://github.com/ampas/CTL
29 |
30 | ## License ##
31 | This project is licensed under the terms of the [LICENSE](./LICENSE.md) agreement.
32 |
33 | ## Contributing ##
34 | Thank you for your interest in contributing to our project. Before any contributions can be accepted, we require contributors to sign a Contributor License Agreement (CLA) to ensure that the project can freely use your contributions. You can find more details and instructions on how to sign the CLA in the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
35 |
36 | ## Support ##
37 | For support, please visit [ACESCentral.com](https://acescentral.com)
38 |
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support
2 |
3 | For support, please visit [ACESCentral.com](https://acescentral.com)
--------------------------------------------------------------------------------
/lib/Lib.Academy.ColorSpaces.ctl:
--------------------------------------------------------------------------------
1 |
2 | // urn:ampas:aces:transformId:v2.0:Lib.Academy.ColorSpaces.a2.v1
3 | // Color Space Conversion
4 |
5 | //
6 | // Functions for assorted color space operations
7 | //
8 |
9 |
10 |
11 | /* ---- Conversion Functions ---- */
12 | // Various transformations between color encodings and data representations
13 | //
14 |
15 | // Transformations between CIE XYZ tristimulus values and CIE x,y
16 | // chromaticity coordinates
17 | float[3] XYZ_2_xyY( float XYZ[3])
18 | {
19 | float xyY[3];
20 | float divisor = (XYZ[0] + XYZ[1] + XYZ[2]);
21 | if (divisor == 0.) divisor = 1e-10;
22 | xyY[0] = XYZ[0] / divisor;
23 | xyY[1] = XYZ[1] / divisor;
24 | xyY[2] = XYZ[1];
25 |
26 | return xyY;
27 | }
28 |
29 | float[3] xyY_2_XYZ( input varying float xyY[3])
30 | {
31 | float XYZ[3];
32 | XYZ[0] = xyY[0] * xyY[2] / max( xyY[1], 1e-10);
33 | XYZ[1] = xyY[2];
34 | XYZ[2] = (1.0 - xyY[0] - xyY[1]) * xyY[2] / max( xyY[1], 1e-10);
35 |
36 | return XYZ;
37 | }
38 |
39 | /* ---- Chromatic Adaptation ---- */
40 |
41 | const float CONE_RESP_MAT_BRADFORD[3][3] = {
42 | { 0.89510, -0.75020, 0.03890},
43 | { 0.26640, 1.71350, -0.06850},
44 | {-0.16140, 0.03670, 1.02960}
45 | };
46 |
47 | const float CONE_RESP_MAT_CAT02[3][3] = {
48 | { 0.73280, -0.70360, 0.00300},
49 | { 0.42960, 1.69750, 0.01360},
50 | {-0.16240, 0.00610, 0.98340}
51 | };
52 |
53 | float[3][3] calculate_cat_matrix
54 | ( float src_xy[2], // x,y chromaticity of source white
55 | float des_xy[2], // x,y chromaticity of destination white
56 | float coneRespMat[3][3] = CONE_RESP_MAT_BRADFORD
57 | )
58 | {
59 | //
60 | // Calculates and returns a 3x3 Von Kries chromatic adaptation transform
61 | // from src_xy to des_xy using the cone response primaries defined
62 | // by coneRespMat. By default, coneRespMat is set to CONE_RESP_MAT_BRADFORD.
63 | // The default coneRespMat can be overridden at runtime.
64 | //
65 |
66 | const float src_xyY[3] = { src_xy[0], src_xy[1], 1. };
67 | const float des_xyY[3] = { des_xy[0], des_xy[1], 1. };
68 |
69 | float src_XYZ[3] = xyY_2_XYZ( src_xyY );
70 | float des_XYZ[3] = xyY_2_XYZ( des_xyY );
71 |
72 | float src_coneResp[3] = mult_f3_f33( src_XYZ, coneRespMat);
73 | float des_coneResp[3] = mult_f3_f33( des_XYZ, coneRespMat);
74 |
75 | float vkMat[3][3] = {
76 | { des_coneResp[0] / src_coneResp[0], 0.0, 0.0 },
77 | { 0.0, des_coneResp[1] / src_coneResp[1], 0.0 },
78 | { 0.0, 0.0, des_coneResp[2] / src_coneResp[2] }
79 | };
80 |
81 | float cat_matrix[3][3] = mult_f33_f33( coneRespMat, mult_f33_f33( vkMat, invert_f33( coneRespMat ) ) );
82 |
83 | return cat_matrix;
84 | }
85 |
86 | float[3][3] calculate_rgb_to_rgb_matrix
87 | ( Chromaticities SOURCE_PRIMARIES,
88 | Chromaticities DEST_PRIMARIES,
89 | float coneRespMat[3][3] = CONE_RESP_MAT_BRADFORD
90 | )
91 | {
92 | //
93 | // Calculates and returns a 3x3 RGB-to-RGB matrix from the source primaries to the
94 | // destination primaries. The returned matrix is effectively a concatenation of a
95 | // conversion of the source RGB values into CIE XYZ tristimulus values, conversion to
96 | // cone response values or other space in which reconciliation of the encoding white is
97 | // done, a conversion back to CIE XYZ tristimulus values, and finally conversion from
98 | // CIE XYZ tristimulus values to the destination RGB values.
99 | //
100 | // By default, coneRespMat is set to CONE_RESP_MAT_BRADFORD.
101 | // The default coneRespMat can be overridden at runtime.
102 | //
103 |
104 | const float RGBtoXYZ_44[4][4] = RGBtoXYZ( SOURCE_PRIMARIES, 1.0);
105 | const float RGBtoXYZ_MAT[3][3] =
106 | { {RGBtoXYZ_44[0][0], RGBtoXYZ_44[0][1], RGBtoXYZ_44[0][2]},
107 | {RGBtoXYZ_44[1][0], RGBtoXYZ_44[1][1], RGBtoXYZ_44[1][2]},
108 | {RGBtoXYZ_44[2][0], RGBtoXYZ_44[2][1], RGBtoXYZ_44[2][2]} };
109 |
110 | // Chromatic adaptation from source white to destination white chromaticity
111 | // Bradford cone response matrix is the default method
112 | const float CAT[3][3] = calculate_cat_matrix( SOURCE_PRIMARIES.white,
113 | DEST_PRIMARIES.white,
114 | coneRespMat );
115 |
116 | const float XYZtoRGB_44[4][4] = XYZtoRGB( DEST_PRIMARIES, 1.0);
117 | const float XYZtoRGB_MAT[3][3] =
118 | { {XYZtoRGB_44[0][0], XYZtoRGB_44[0][1], XYZtoRGB_44[0][2]},
119 | {XYZtoRGB_44[1][0], XYZtoRGB_44[1][1], XYZtoRGB_44[1][2]},
120 | {XYZtoRGB_44[2][0], XYZtoRGB_44[2][1], XYZtoRGB_44[2][2]}};
121 |
122 | return mult_f33_f33( RGBtoXYZ_MAT, mult_f33_f33( CAT, XYZtoRGB_MAT));
123 | }
--------------------------------------------------------------------------------
/lib/Lib.Academy.DisplayEncoding.ctl:
--------------------------------------------------------------------------------
1 |
2 | // urn:ampas:aces:transformId:v2.0:Lib.Academy.DisplayEncoding.a2.v1
3 | // Display Encoding Functions
4 |
5 | //
6 | // Library File with functions used for the display encoding/decoding steps
7 | //
8 |
9 | // White point scaling
10 | // If the creative white differs from the calibration white of the display,
11 | // unequal display code values will be required to produce the neutral of
12 | // the creative white. Without scaling, one channel would hit the max value
13 | // first while the other channels continue to increase, resulting in a hue
14 | // shift. To avoid this, the white scaling finds the largest channel and
15 | // applies a scale factor to force the point where this channel hits max to
16 | // 1.0, assuring that all three channels "fit" within the peak value. In the
17 | // inverse direction, the white scaling is removed.
18 | float[3] apply_white_scale(float rgb[3],
19 | float MAT_limit_to_display[3][3],
20 | bool invert)
21 | {
22 | float RGB_w_f[3] = mult_f3_f33(f3_from_f(1.), MAT_limit_to_display);
23 | float scale = 1. / max(max(RGB_w_f[0], RGB_w_f[1]), RGB_w_f[2]);
24 | // scale factor is equal to 1/largestChannel
25 |
26 | if (invert)
27 | {
28 | return clamp_f3(mult_f_f3(1. / scale, rgb), 0, 1);
29 | }
30 | else
31 | {
32 | return mult_f_f3(scale, rgb);
33 | }
34 | }
35 |
36 | // Forward monitor curve - moncurve_f() with gamma=2.4 and offset=0.055 matches the inverse EOTF found in IEC 61966-2-1:1999 (sRGB)
37 | float moncurve_fwd(float x,
38 | float gamma,
39 | float offs)
40 | {
41 | float y;
42 | const float fs = ((gamma - 1.0) / offs) * pow(offs * gamma / ((gamma - 1.0) * (1.0 + offs)), gamma);
43 | const float xb = offs / (gamma - 1.0);
44 | if (x >= xb)
45 | {
46 | y = pow((x + offs) / (1.0 + offs), gamma);
47 | }
48 | else
49 | {
50 | y = x * fs;
51 | }
52 | return y;
53 | }
54 |
55 | // Reverse monitor curve - moncurve_r() with gamma=2.4 and offset=0.055 matches the inverse EOTF found in IEC 61966-2-1:1999 (sRGB)
56 | float moncurve_inv(float y,
57 | float gamma,
58 | float offs)
59 | {
60 | float x;
61 | const float yb = pow(offs * gamma / ((gamma - 1.0) * (1.0 + offs)), gamma);
62 | const float rs = pow((gamma - 1.0) / offs, gamma - 1.0) * pow((1.0 + offs) / gamma, gamma);
63 | if (y >= yb)
64 | {
65 | x = (1.0 + offs) * pow(y, 1.0 / gamma) - offs;
66 | }
67 | else
68 | {
69 | x = y * rs;
70 | }
71 | return x;
72 | }
73 |
74 | float[3] moncurve_fwd_f3(float x[3],
75 | float gamma,
76 | float offs)
77 | {
78 | float y[3];
79 | y[0] = moncurve_fwd(x[0], gamma, offs);
80 | y[1] = moncurve_fwd(x[1], gamma, offs);
81 | y[2] = moncurve_fwd(x[2], gamma, offs);
82 | return y;
83 | }
84 |
85 | float[3] moncurve_inv_f3(float y[3],
86 | float gamma,
87 | float offs)
88 | {
89 | float x[3];
90 | x[0] = moncurve_inv(y[0], gamma, offs);
91 | x[1] = moncurve_inv(y[1], gamma, offs);
92 | x[2] = moncurve_inv(y[2], gamma, offs);
93 | return x;
94 | }
95 |
96 | // The forward OTF specified in Rec. ITU-R BT.1886
97 | // L = a(max[(V+b),0])^g
98 | float bt1886_fwd(float V,
99 | float gamma,
100 | float Lw = 1.0,
101 | float Lb = 0.0)
102 | {
103 | float a = pow(pow(Lw, 1. / gamma) - pow(Lb, 1. / gamma), gamma);
104 | float b = pow(Lb, 1. / gamma) / (pow(Lw, 1. / gamma) - pow(Lb, 1. / gamma));
105 | float L = a * pow(max(V + b, 0.), gamma);
106 | return L;
107 | }
108 |
109 | // The reverse EOTF specified in Rec. ITU-R BT.1886
110 | // L = a(max[(V+b),0])^g
111 | float bt1886_inv(float L,
112 | float gamma,
113 | float Lw = 1.0,
114 | float Lb = 0.0)
115 | {
116 | float a = pow(pow(Lw, 1. / gamma) - pow(Lb, 1. / gamma), gamma);
117 | float b = pow(Lb, 1. / gamma) / (pow(Lw, 1. / gamma) - pow(Lb, 1. / gamma));
118 | float V = pow(max(L / a, 0.), 1. / gamma) - b;
119 | return V;
120 | }
121 |
122 | float[3] bt1886_fwd_f3(float V[3],
123 | float gamma,
124 | float Lw = 1.0,
125 | float Lb = 0.0)
126 | {
127 | float L[3];
128 | L[0] = bt1886_fwd(V[0], gamma, Lw, Lb);
129 | L[1] = bt1886_fwd(V[1], gamma, Lw, Lb);
130 | L[2] = bt1886_fwd(V[2], gamma, Lw, Lb);
131 | return L;
132 | }
133 |
134 | float[3] bt1886_inv_f3(float L[3],
135 | float gamma,
136 | float Lw = 1.0,
137 | float Lb = 0.0)
138 | {
139 | float V[3];
140 | V[0] = bt1886_inv(L[0], gamma, Lw, Lb);
141 | V[1] = bt1886_inv(L[1], gamma, Lw, Lb);
142 | V[2] = bt1886_inv(L[2], gamma, Lw, Lb);
143 | return V;
144 | }
145 |
146 | // SMPTE Range vs Full Range scaling formulas
147 | float smpteRange_to_fullRange(float in)
148 | {
149 | const float REFBLACK = (64. / 1023.);
150 | const float REFWHITE = (940. / 1023.);
151 |
152 | return ((in - REFBLACK) / (REFWHITE - REFBLACK));
153 | }
154 |
155 | float fullRange_to_smpteRange(float in)
156 | {
157 | const float REFBLACK = (64. / 1023.);
158 | const float REFWHITE = (940. / 1023.);
159 |
160 | return (in * (REFWHITE - REFBLACK) + REFBLACK);
161 | }
162 |
163 | float[3] smpteRange_to_fullRange_f3(float rgbIn[3])
164 | {
165 | float rgbOut[3];
166 | rgbOut[0] = smpteRange_to_fullRange(rgbIn[0]);
167 | rgbOut[1] = smpteRange_to_fullRange(rgbIn[1]);
168 | rgbOut[2] = smpteRange_to_fullRange(rgbIn[2]);
169 |
170 | return rgbOut;
171 | }
172 |
173 | float[3] fullRange_to_smpteRange_f3(float rgbIn[3])
174 | {
175 | float rgbOut[3];
176 | rgbOut[0] = fullRange_to_smpteRange(rgbIn[0]);
177 | rgbOut[1] = fullRange_to_smpteRange(rgbIn[1]);
178 | rgbOut[2] = fullRange_to_smpteRange(rgbIn[2]);
179 |
180 | return rgbOut;
181 | }
182 |
183 | // Base functions from SMPTE ST 2084-2014
184 |
185 | // Constants from SMPTE ST 2084-2014
186 | const float pq_m1 = 0.1593017578125; // ( 2610.0 / 4096.0 ) / 4.0;
187 | const float pq_m2 = 78.84375; // ( 2523.0 / 4096.0 ) * 128.0;
188 | const float pq_c1 = 0.8359375; // 3424.0 / 4096.0 or pq_c3 - pq_c2 + 1.0;
189 | const float pq_c2 = 18.8515625; // ( 2413.0 / 4096.0 ) * 32.0;
190 | const float pq_c3 = 18.6875; // ( 2392.0 / 4096.0 ) * 32.0;
191 |
192 | const float pq_C = 10000.0;
193 |
194 | // Converts from the non-linear perceptually quantized space to linear cd/m^2
195 | // Note that this is in float, and assumes normalization from 0 - 1
196 | // (0 - pq_C for linear) and does not handle the integer coding in the Annex
197 | // sections of SMPTE ST 2084-2014
198 | float ST2084_to_Y(float N)
199 | {
200 | // Note that this does NOT handle any of the signal range
201 | // considerations from 2084 - this assumes full range (0 - 1)
202 | float Np = pow(N, 1.0 / pq_m2);
203 | float L = Np - pq_c1;
204 | if (L < 0.0)
205 | L = 0.0;
206 | L = L / (pq_c2 - pq_c3 * Np);
207 | L = pow(L, 1.0 / pq_m1);
208 | return L * pq_C; // returns cd/m^2
209 | }
210 |
211 | // Converts from linear cd/m^2 to the non-linear perceptually quantized space
212 | // Note that this is in float, and assumes normalization from 0 - 1
213 | // (0 - pq_C for linear) and does not handle the integer coding in the Annex
214 | // sections of SMPTE ST 2084-2014
215 | float Y_to_ST2084(float C)
216 | {
217 | // Note that this does NOT handle any of the signal range
218 | // considerations from 2084 - this returns full range (0 - 1)
219 | float L = C / pq_C;
220 | float Lm = pow(L, pq_m1);
221 | float N = (pq_c1 + pq_c2 * Lm) / (1.0 + pq_c3 * Lm);
222 | N = pow(N, pq_m2);
223 | return N;
224 | }
225 |
226 | // converts from linear cd/m^2 to PQ code values
227 | float[3] Y_to_ST2084_f3(float in[3])
228 | {
229 | float out[3];
230 | out[0] = Y_to_ST2084(in[0]);
231 | out[1] = Y_to_ST2084(in[1]);
232 | out[2] = Y_to_ST2084(in[2]);
233 |
234 | return out;
235 | }
236 |
237 | // converts from PQ code values to linear cd/m^2
238 | float[3] ST2084_to_Y_f3(float in[3])
239 | {
240 | float out[3];
241 | out[0] = ST2084_to_Y(in[0]);
242 | out[1] = ST2084_to_Y(in[1]);
243 | out[2] = ST2084_to_Y(in[2]);
244 |
245 | return out;
246 | }
247 |
248 | // Conversion of PQ signal to HLG, as detailed in Section 7 of ITU-R BT.2390-0
249 | float[3] ST2084_to_HLG_1000nits_f3(float PQ[3])
250 | {
251 | // ST.2084 EOTF (non-linear PQ to display light)
252 | float displayLinear[3] = ST2084_to_Y_f3(PQ);
253 |
254 | // HLG Inverse EOTF (i.e. HLG inverse OOTF followed by the HLG OETF)
255 | // HLG Inverse OOTF (display linear to scene linear)
256 | float Y_d = 0.2627 * displayLinear[0] + 0.6780 * displayLinear[1] + 0.0593 * displayLinear[2];
257 | const float L_w = 1000.;
258 | const float L_b = 0.;
259 | const float alpha = (L_w - L_b);
260 | const float beta = L_b;
261 | const float gamma = 1.2;
262 |
263 | float sceneLinear[3];
264 | if (Y_d == 0.)
265 | {
266 | /* This case is to protect against pow(0,-N)=Inf error. The ITU document
267 | does not offer a recommendation for this corner case. There may be a
268 | better way to handle this, but for now, this works.
269 | */
270 | sceneLinear[0] = 0.;
271 | sceneLinear[1] = 0.;
272 | sceneLinear[2] = 0.;
273 | }
274 | else
275 | {
276 | sceneLinear[0] = pow((Y_d - beta) / alpha, (1. - gamma) / gamma) * ((displayLinear[0] - beta) / alpha);
277 | sceneLinear[1] = pow((Y_d - beta) / alpha, (1. - gamma) / gamma) * ((displayLinear[1] - beta) / alpha);
278 | sceneLinear[2] = pow((Y_d - beta) / alpha, (1. - gamma) / gamma) * ((displayLinear[2] - beta) / alpha);
279 | }
280 |
281 | // HLG OETF (scene linear to non-linear signal value)
282 | const float a = 0.17883277;
283 | const float b = 0.28466892; // 1.-4.*a;
284 | const float c = 0.55991073; // 0.5-a*log(4.*a);
285 |
286 | float HLG[3];
287 | if (sceneLinear[0] <= 1. / 12)
288 | {
289 | HLG[0] = sqrt(3. * sceneLinear[0]);
290 | }
291 | else
292 | {
293 | HLG[0] = a * log(12. * sceneLinear[0] - b) + c;
294 | }
295 | if (sceneLinear[1] <= 1. / 12)
296 | {
297 | HLG[1] = sqrt(3. * sceneLinear[1]);
298 | }
299 | else
300 | {
301 | HLG[1] = a * log(12. * sceneLinear[1] - b) + c;
302 | }
303 | if (sceneLinear[2] <= 1. / 12)
304 | {
305 | HLG[2] = sqrt(3. * sceneLinear[2]);
306 | }
307 | else
308 | {
309 | HLG[2] = a * log(12. * sceneLinear[2] - b) + c;
310 | }
311 |
312 | return HLG;
313 | }
314 |
315 | // Conversion of HLG to PQ signal, as detailed in Section 7 of ITU-R BT.2390-0
316 | float[3] HLG_to_ST2084_1000nits_f3(float HLG[3])
317 | {
318 | const float a = 0.17883277;
319 | const float b = 0.28466892; // 1.-4.*a;
320 | const float c = 0.55991073; // 0.5-a*log(4.*a);
321 |
322 | const float L_w = 1000.;
323 | const float L_b = 0.;
324 | const float alpha = (L_w - L_b);
325 | const float beta = L_b;
326 | const float gamma = 1.2;
327 |
328 | // HLG EOTF (non-linear signal value to display linear)
329 | // HLG to scene-linear
330 | float sceneLinear[3];
331 | if (HLG[0] >= 0. && HLG[0] <= 0.5)
332 | {
333 | sceneLinear[0] = pow(HLG[0], 2.) / 3.;
334 | }
335 | else
336 | {
337 | sceneLinear[0] = (exp((HLG[0] - c) / a) + b) / 12.;
338 | }
339 | if (HLG[1] >= 0. && HLG[1] <= 0.5)
340 | {
341 | sceneLinear[1] = pow(HLG[1], 2.) / 3.;
342 | }
343 | else
344 | {
345 | sceneLinear[1] = (exp((HLG[1] - c) / a) + b) / 12.;
346 | }
347 | if (HLG[2] >= 0. && HLG[2] <= 0.5)
348 | {
349 | sceneLinear[2] = pow(HLG[2], 2.) / 3.;
350 | }
351 | else
352 | {
353 | sceneLinear[2] = (exp((HLG[2] - c) / a) + b) / 12.;
354 | }
355 |
356 | float Y_s = 0.2627 * sceneLinear[0] + 0.6780 * sceneLinear[1] + 0.0593 * sceneLinear[2];
357 |
358 | // Scene-linear to display-linear
359 | float displayLinear[3];
360 | displayLinear[0] = alpha * pow(Y_s, gamma - 1.) * sceneLinear[0] + beta;
361 | displayLinear[1] = alpha * pow(Y_s, gamma - 1.) * sceneLinear[1] + beta;
362 | displayLinear[2] = alpha * pow(Y_s, gamma - 1.) * sceneLinear[2] + beta;
363 |
364 | // ST.2084 Inverse EOTF
365 | float PQ[3] = Y_to_ST2084_f3(displayLinear);
366 |
367 | return PQ;
368 | }
369 |
370 | // 0 - display linear
371 | // 1 - ST.2084
372 | // 2 - HLG
373 | // 3 - gamma 2.6
374 | // 4 - BT.1886 with gamma 2.4
375 | // 5 - gamma 2.2
376 | // 6 - sRGB IEC 61966-2-1:1999
377 |
378 | float[3] eotf_inv(float rgb_linear_in[3],
379 | int eotf_enum)
380 | {
381 | // Extra clamp of negatives protect against edge case negative values. Data
382 | // is clamped from 0-peakLuminance in the white limiting step but sometimes
383 | // a very small negative value can be reintroduced from precision errors.
384 | float rgb_linear[3] = rgb_linear_in;
385 | rgb_linear[0] = max(0.0, rgb_linear_in[0]);
386 | rgb_linear[1] = max(0.0, rgb_linear_in[1]);
387 | rgb_linear[2] = max(0.0, rgb_linear_in[2]);
388 |
389 | if (eotf_enum == 0)
390 | {
391 | // display linear
392 | return rgb_linear;
393 | }
394 | else if (eotf_enum == 1)
395 | {
396 | // ST. 2084
397 | return Y_to_ST2084_f3(mult_f_f3(ref_luminance, rgb_linear));
398 | }
399 | else if (eotf_enum == 2)
400 | {
401 | // HLG
402 | float PQ[3] = Y_to_ST2084_f3(mult_f_f3(ref_luminance, rgb_linear));
403 | return ST2084_to_HLG_1000nits_f3(PQ);
404 | }
405 | else if (eotf_enum == 3)
406 | {
407 | // gamma 2.6
408 | return pow_f3(rgb_linear, 1 / 2.6);
409 | }
410 | else if (eotf_enum == 4)
411 | {
412 | // BT.1886 with gamma 2.4
413 | return bt1886_inv_f3(rgb_linear, 2.4, 1.0, 0.0);
414 | }
415 | else if (eotf_enum == 5)
416 | {
417 | // gamma 2.2
418 | return pow_f3(rgb_linear, 1 / 2.2);
419 | }
420 | else
421 | {
422 | // sRGB IEC 61966-2-1:1999
423 | return moncurve_inv_f3(rgb_linear, 2.4, 0.055);
424 | }
425 | }
426 |
427 | float[3] eotf(float rgb_cv[3],
428 | int eotf_enum)
429 | {
430 | if (eotf_enum == 0)
431 | {
432 | // display linear
433 | return rgb_cv;
434 | }
435 | else if (eotf_enum == 1)
436 | {
437 | // ST. 2084
438 | return mult_f_f3(1. / ref_luminance, ST2084_to_Y_f3(rgb_cv));
439 | }
440 | else if (eotf_enum == 2)
441 | {
442 | // HLG
443 | float PQ[3] = HLG_to_ST2084_1000nits_f3(rgb_cv);
444 | return mult_f_f3(1 / ref_luminance, ST2084_to_Y_f3(PQ));
445 | }
446 | else if (eotf_enum == 3)
447 | {
448 | // gamma 2.6
449 | return pow_f3(rgb_cv, 2.6);
450 | }
451 | else if (eotf_enum == 4)
452 | {
453 | // BT.1886 with gamma 2.4
454 | return bt1886_fwd_f3(rgb_cv, 2.4, 1.0, 0.0);
455 | }
456 | else if (eotf_enum == 5)
457 | {
458 | // gamma 2.2
459 | return pow_f3(rgb_cv, 2.2);
460 | }
461 | else
462 | {
463 | // sRGB IEC 61966-2-1:1999
464 | return moncurve_fwd_f3(rgb_cv, 2.4, 0.055);
465 | }
466 | }
467 |
468 | float[3] display_encoding(float rgb[3],
469 | float MAT_limit_to_display[3][3],
470 | int eotf_enum,
471 | float linear_scale = 1.0)
472 | {
473 | // Limiting to display primary encoding
474 | float rgb_display_linear[3] = mult_f3_f33(rgb, MAT_limit_to_display);
475 |
476 | // Linear scale factor
477 | float rgb_display_scaled[3] = mult_f_f3(linear_scale, rgb_display_linear);
478 |
479 | // Apply inverse EOTF
480 | float cv[3] = eotf_inv(rgb_display_scaled, eotf_enum);
481 |
482 | return cv;
483 | }
484 |
485 | float[3] display_decoding(float cv[3],
486 | float MAT_display_to_limit[3][3],
487 | int eotf_enum,
488 | float linear_scale = 1.0)
489 | {
490 | // Apply EOTF
491 | float rgb_display_scaled[3] = eotf(cv, eotf_enum);
492 |
493 | // Linear scale factor
494 | float rgb_display_linear[3] = mult_f_f3(1. / linear_scale, rgb_display_scaled);
495 |
496 | // Display to limiting primary encoding
497 | float rgb_limit[3] = mult_f3_f33(rgb_display_linear, MAT_display_to_limit);
498 |
499 | return rgb_limit;
500 | }
--------------------------------------------------------------------------------
/lib/Lib.Academy.OutputTransform.ctl:
--------------------------------------------------------------------------------
1 |
2 | // urn:ampas:aces:transformId:v2.0:Lib.Academy.OutputTransform.a2.v1
3 | // Output Transform Functions
4 |
5 | //
6 | // Library File with functions and presets used for the forward and inverse output
7 | // transform
8 | //
9 |
10 | // Chromaticities & Conversion matrices
11 | // Academy Primaries 0 (i.e. "ACES" Primaries from SMPTE ST2065-1)
12 | const Chromaticities AP0 =
13 | {
14 | {0.73470, 0.26530},
15 | {0.00000, 1.00000},
16 | {0.00010, -0.07700},
17 | {0.32168, 0.33767}};
18 |
19 | const float AP0_XYZ_TO_RGB[3][3] = XYZtoRGB_f33(AP0, 1.0);
20 | const float AP0_RGB_TO_XYZ[3][3] = RGBtoXYZ_f33(AP0, 1.0);
21 |
22 | // Academy Primaries 1
23 | const Chromaticities AP1 =
24 | {
25 | {0.713, 0.293},
26 | {0.165, 0.830},
27 | {0.128, 0.044},
28 | {0.32168, 0.33767}};
29 |
30 | const float AP1_XYZ_TO_RGB[3][3] = XYZtoRGB_f33(AP1, 1.0);
31 | const float AP1_RGB_TO_XYZ[3][3] = RGBtoXYZ_f33(AP1, 1.0);
32 |
33 | const float AP0_TO_AP1[3][3] = mult_f33_f33(AP0_RGB_TO_XYZ, AP1_XYZ_TO_RGB);
34 | const float AP1_TO_AP0[3][3] = mult_f33_f33(AP1_RGB_TO_XYZ, AP0_XYZ_TO_RGB);
35 |
36 | // "Reach" Primaries - equal to ACES "AP1" primaries
37 | const Chromaticities REACH_PRI = AP1;
38 |
39 | // Table generation
40 | const int tableSize = 360;
41 | const int additionalTableEntries = 2; // allots for extra entries to wrap the hues
42 | const int totalTableSize = tableSize + additionalTableEntries;
43 | const int baseIndex = 1; // array index for smallest filled entry of padded table
44 |
45 | const float hue_limit = 360.;
46 |
47 | const int cuspCornerCount = 6;
48 | const int totalCornerCount = cuspCornerCount + 2;
49 | const int max_sorted_corners = 2 * cuspCornerCount;
50 | const float reach_cusp_tolerance = 1e-3;
51 | const float display_cusp_tolerance = 1e-7;
52 |
53 | const float gamma_minimum = 0.0;
54 | const float gamma_maximum = 5.0;
55 | const float gamma_search_step = 0.4;
56 | const float gamma_accuracy = 1e-5;
57 |
58 | // CAM Parameters
59 | const float ref_luminance = 100.;
60 | const float L_A = 100.;
61 | const float Y_b = 20.;
62 | const float surround[3] = {0.9, 0.59, 0.9}; // Dim surround
63 |
64 | const float J_scale = 100.0;
65 | const float cam_nl_Y_reference = 100.0;
66 | const float cam_nl_offset = 0.2713 * cam_nl_Y_reference;
67 | const float cam_nl_scale = 4.0 * cam_nl_Y_reference;
68 |
69 | const float model_gamma = surround[1] * (1.48 + sqrt(Y_b / ref_luminance));
70 |
71 | // Chroma compression
72 | const float chroma_compress = 2.4;
73 | const float chroma_compress_fact = 3.3;
74 | const float chroma_expand = 1.3;
75 | const float chroma_expand_fact = 0.69;
76 | const float chroma_expand_thr = 0.5;
77 |
78 | // Gamut compression
79 | const float smooth_cusps = 0.12;
80 | const float smooth_m = 0.27;
81 | const float cusp_mid_blend = 1.3;
82 |
83 | const float focus_gain_blend = 0.3;
84 | const float focus_adjust_gain = 0.55;
85 | const float focus_distance = 1.35;
86 | const float focus_distance_scaling = 1.75;
87 |
88 | const float compression_threshold = 0.75;
89 |
90 | const float MATRIX_IDENTITY[3][3] = {
91 | {1., 0, 0},
92 | {0, 1., 0},
93 | {0, 0, 1.}};
94 |
95 | struct JMhParams
96 | {
97 | // Pre-computed conversion matrices and constants for conversions to/from JMh
98 | float MATRIX_RGB_to_CAM16_c[3][3];
99 | float MATRIX_CAM16_c_to_RGB[3][3];
100 | float MATRIX_cone_response_to_Aab[3][3];
101 | float MATRIX_Aab_to_cone_response[3][3];
102 | float F_L_n; // F_L normalised
103 | float cz;
104 | float inv_cz; // 1/cz
105 | float A_w_J;
106 | float inv_A_w_J; // 1/A_w_J
107 | };
108 |
109 | struct HueDependentGamutParams
110 | {
111 | // Hue-dependent gamut parameters
112 | float JMcusp[2];
113 | float gamma_bottom_inv;
114 | float gamma_top_inv;
115 | float focus_J;
116 | float analytical_threshold;
117 | };
118 |
119 | struct ODTParams
120 | {
121 | float peakLuminance;
122 |
123 | // JMh parameters
124 | JMhParams input_params;
125 | JMhParams reach_params;
126 | JMhParams limit_params;
127 |
128 | // Tonescale parameters
129 | TSParams ts;
130 |
131 | // Shared compression parameters
132 | float limit_J_max;
133 | float model_gamma_inv;
134 | float TABLE_reach_M[totalTableSize];
135 |
136 | // Chroma compression parameters
137 | float sat;
138 | float sat_thr;
139 | float compr;
140 | float chroma_compress_scale;
141 |
142 | // Gamut compression parameters
143 | float mid_J;
144 | float focus_dist;
145 | float lower_hull_gamma_inv;
146 | float TABLE_hues[totalTableSize];
147 | float TABLE_gamut_cusps[totalTableSize][3];
148 | float TABLE_upper_hull_gamma[totalTableSize];
149 | int hue_linearity_search_range[2];
150 |
151 | };
152 |
153 | float wrap_to_360(float hue)
154 | {
155 | float y = fmod(hue, 360.);
156 | if (y < 0.)
157 | {
158 | y = y + 360.;
159 | }
160 | return y;
161 | }
162 |
163 | int hue_position_in_uniform_table(float hue, int table_size)
164 | {
165 | const float wrapped_hue = wrap_to_360(hue);
166 | int result = (wrapped_hue / hue_limit * table_size);
167 | return result;
168 | }
169 |
170 | int next_position_in_table(int entry, int table_size)
171 | {
172 | int result = (entry + 1) % table_size;
173 | return result;
174 | }
175 |
176 | float base_hue_for_position(int i_lo, int table_size)
177 | {
178 | float result = i_lo * hue_limit / table_size;
179 | return result;
180 | }
181 |
182 | // CAM Functions
183 | float _post_adaptation_cone_response_compression_fwd(float Rc)
184 | {
185 | const float F_L_Y = pow(Rc, 0.42);
186 | const float Ra = (F_L_Y) / (cam_nl_offset + F_L_Y);
187 | return Ra;
188 | }
189 |
190 | float _post_adaptation_cone_response_compression_inv(float Ra)
191 | {
192 | const float Ra_lim = min(Ra, 0.99);
193 | const float F_L_Y = (cam_nl_offset * Ra_lim) / (1. - Ra_lim);
194 | const float Rc = pow(F_L_Y, 1. / 0.42);
195 | return Rc;
196 | }
197 |
198 | float post_adaptation_cone_response_compression_fwd(float v)
199 | {
200 | const float abs_v = fabs(v);
201 | const float Ra = _post_adaptation_cone_response_compression_fwd(abs_v);
202 | return copysign(Ra, v);
203 | }
204 |
205 | float post_adaptation_cone_response_compression_inv(float v)
206 | {
207 | const float abs_v = fabs(v);
208 | const float Rc = _post_adaptation_cone_response_compression_inv(abs_v);
209 | return copysign(Rc, v);
210 | }
211 |
212 | float Achromatic_n_to_J(float A,
213 | float cz)
214 | {
215 | return J_scale * pow(A, cz);
216 | }
217 |
218 | float J_to_Achromatic_n(float J,
219 | float inv_cz)
220 | {
221 | return pow(J * (1. / J_scale), inv_cz);
222 | }
223 |
224 | // Optimization for achromatic values
225 | float _A_to_Y(float A,
226 | JMhParams p)
227 | {
228 | float Ra = p.A_w_J * A;
229 | float Y = _post_adaptation_cone_response_compression_inv(Ra) / p.F_L_n;
230 |
231 | return Y;
232 | }
233 |
234 | float J_to_Y(float J,
235 | JMhParams p)
236 | {
237 | float abs_J = fabs(J);
238 |
239 | return _A_to_Y(J_to_Achromatic_n(abs_J, p.inv_cz), p);
240 | }
241 |
242 | float Y_to_J(float Y,
243 | JMhParams p)
244 | {
245 | float abs_Y = fabs(Y);
246 | float Ra = _post_adaptation_cone_response_compression_fwd(abs_Y * p.F_L_n);
247 | float J = Achromatic_n_to_J(Ra * p.inv_A_w_J, p.cz);
248 |
249 | return copysign(J, Y);
250 | }
251 |
252 | float[3] RGB_to_Aab(float RGB[3],
253 | JMhParams p)
254 | {
255 | float rgb_m[3] = mult_f3_f33(RGB, p.MATRIX_RGB_to_CAM16_c);
256 |
257 | float rgb_a[3] = {
258 | post_adaptation_cone_response_compression_fwd(rgb_m[0]),
259 | post_adaptation_cone_response_compression_fwd(rgb_m[1]),
260 | post_adaptation_cone_response_compression_fwd(rgb_m[2])};
261 |
262 | float Aab[3] = mult_f3_f33(rgb_a, p.MATRIX_cone_response_to_Aab);
263 |
264 | return Aab;
265 | }
266 |
267 | float[3] Aab_to_JMh(float Aab[3],
268 | JMhParams p)
269 | {
270 | float JMh[3] = {0., 0., 0.};
271 | if (Aab[0] <= 0.)
272 | {
273 | return JMh;
274 | }
275 | float J = Achromatic_n_to_J(Aab[0], p.cz);
276 | float M = sqrt(Aab[1] * Aab[1] + Aab[2] * Aab[2]);
277 | float h_rad = atan2(Aab[2], Aab[1]);
278 | float h = wrap_to_360(radians_to_degrees(h_rad));
279 |
280 | JMh[0] = J;
281 | JMh[1] = M;
282 | JMh[2] = h;
283 |
284 | return JMh;
285 | }
286 |
287 | float[3] RGB_to_JMh(float RGB[3],
288 | JMhParams p)
289 | {
290 | float Aab[3] = RGB_to_Aab(RGB, p);
291 | float JMh[3] = Aab_to_JMh(Aab, p);
292 |
293 | return JMh;
294 | }
295 |
296 | float[3] JMh_to_Aab(float JMh[3],
297 | JMhParams p)
298 | {
299 | float J = JMh[0];
300 | float M = JMh[1];
301 | float h = JMh[2];
302 | float h_rad = degrees_to_radians(h);
303 | float cos_hr = cos(h_rad);
304 | float sin_hr = sin(h_rad);
305 |
306 | float A = J_to_Achromatic_n(J, p.inv_cz);
307 | float a = M * cos_hr;
308 | float b = M * sin_hr;
309 | float Aab[3] = {A, a, b};
310 |
311 | return Aab;
312 | }
313 |
314 | float[3] Aab_to_RGB(float Aab[3],
315 | JMhParams p)
316 | {
317 | float rgb_a[3] = mult_f3_f33(Aab, p.MATRIX_Aab_to_cone_response);
318 |
319 | float rgb_m[3] = {
320 | post_adaptation_cone_response_compression_inv(rgb_a[0]),
321 | post_adaptation_cone_response_compression_inv(rgb_a[1]),
322 | post_adaptation_cone_response_compression_inv(rgb_a[2])};
323 |
324 | float rgb[3] = mult_f3_f33(rgb_m, p.MATRIX_CAM16_c_to_RGB);
325 |
326 | return rgb;
327 | }
328 |
329 | float[3] JMh_to_RGB(float JMh[3],
330 | JMhParams p)
331 | {
332 | float Aab[3] = JMh_to_Aab(JMh, p);
333 | float rgb[3] = Aab_to_RGB(Aab, p);
334 | return rgb;
335 | }
336 |
337 | float[3] clamp_AP0_to_AP1(float aces[3],
338 | float clamp_lower_limit,
339 | float clamp_upper_limit)
340 | {
341 | float AP1[3] = mult_f3_f33(aces, AP0_TO_AP1);
342 | float AP1_clamped[3] = clamp_f3(AP1, clamp_lower_limit, clamp_upper_limit);
343 | float AP0_clamped[3] = mult_f3_f33(AP1_clamped, AP1_TO_AP0);
344 |
345 | return AP0_clamped;
346 | }
347 |
348 | float reach_M_from_table(float h,
349 | float table[])
350 | {
351 | int base = hue_position_in_uniform_table(h, tableSize);
352 | float t = h - base;
353 | int i_lo = base + baseIndex;
354 | int i_hi = i_lo + 1;
355 |
356 | return lerp(table[i_lo], table[i_hi], t);
357 | }
358 |
359 | float reinhard_remap(float scale,
360 | float nd,
361 | bool invert = false)
362 | {
363 | if (invert)
364 | {
365 | if (nd >= 1.0)
366 | {
367 | return scale;
368 | }
369 | else
370 | {
371 | return scale * -(nd / (nd - 1.));
372 | }
373 | }
374 | return scale * nd / (1. + nd);
375 | }
376 |
377 | float midpoint(float low, float high)
378 | {
379 | return (low + high) / 2.;
380 | }
381 |
382 | // A "toe" function that remaps the given value x between 0 and limit.
383 | // The k1 and k2 parameters change the size and shape of the toe.
384 | // https://www.desmos.com/calculator/6vplvw14ti
385 | float toe(float x,
386 | float limit,
387 | float k1_in,
388 | float k2_in,
389 | bool invert = false)
390 | {
391 | if (x > limit)
392 | return x;
393 |
394 | float k2 = max(k2_in, 0.001);
395 | float k1 = sqrt(k1_in * k1_in + k2 * k2);
396 | float k3 = (limit + k1) / (limit + k2);
397 |
398 | if (invert)
399 | {
400 | return (x * x + k1 * x) / (k3 * (x + k2));
401 | }
402 | else
403 | {
404 | const float minus_b = k3 * x - k1;
405 | const float minus_c = k2 * k3 * x;
406 | return 0.5 * (minus_b + sqrt(minus_b * minus_b + 4. * minus_c));
407 | }
408 | }
409 |
410 | float chroma_compress_norm(float h,
411 | float chroma_compress_scale)
412 | {
413 | float hr = degrees_to_radians(h);
414 |
415 | float a = cos(hr);
416 | float b = sin(hr);
417 | float cos_hr2 = a * a - b * b;
418 | float sin_hr2 = 2.0 * a * b;
419 | float cos_hr3 = 4.0 * a * a * a - 3.0 * a;
420 | float sin_hr3 = 3.0 * b - 4.0 * b * b * b;
421 |
422 | float M = 11.34072 * a +
423 | 16.46899 * cos_hr2 +
424 | 7.88380 * cos_hr3 +
425 | 14.66441 * b +
426 | -6.37224 * sin_hr2 +
427 | 9.19364 * sin_hr3 +
428 | 77.12896;
429 |
430 | return M * chroma_compress_scale;
431 | }
432 |
433 | // In-gamut chroma compression
434 | //
435 | // Compresses colors inside the gamut with the aim for colorfulness to have an
436 | // appropriate rate of change from display black to display white, and from
437 | // achromatic outward to purer colors.
438 | float[3] chroma_compress_fwd(float JMh[3],
439 | float tonemapped_J,
440 | ODTParams p,
441 | bool invert = false)
442 | {
443 | float J = JMh[0];
444 | float M = JMh[1];
445 | float h = JMh[2];
446 |
447 | float M_compr = M;
448 |
449 | if (M != 0.0)
450 | {
451 | const float nJ = tonemapped_J / p.limit_J_max;
452 | const float snJ = max(0., 1. - nJ);
453 | float Mnorm = chroma_compress_norm(h, p.chroma_compress_scale);
454 | float limit = pow(nJ, p.model_gamma_inv) * reach_M_from_table(h, p.TABLE_reach_M) / Mnorm;
455 |
456 | float toe_limit = limit - 0.001;
457 | float toe_snJ_sat = snJ * p.sat;
458 | float toe_sqrt_nJ_sat_thr = sqrt(nJ * nJ + p.sat_thr);
459 | float toe_nJ_compr = nJ * p.compr;
460 |
461 | // Rescaling of M with the tonescaled J to get the M to the same range as
462 | // J after the tonescale. The rescaling uses the Hellwig2022 model gamma to
463 | // keep the M/J ratio correct (keeping the chromaticities constant).
464 | M_compr = M * pow(tonemapped_J / J, p.model_gamma_inv);
465 |
466 | // Normalize M with the rendering space cusp M
467 | M_compr = M_compr / Mnorm;
468 |
469 | // Expand the colorfulness by running the toe function in reverse. The goal is to
470 | // expand less saturated colors less and more saturated colors more. The expansion
471 | // increases saturation in the shadows and mid-tones but not in the highlights.
472 | // The 0.001 offset starts the expansions slightly above zero. The sat_thr makes
473 | // the toe less aggressive near black to reduce the expansion of noise.
474 | M_compr = limit - toe(limit - M_compr, toe_limit, toe_snJ_sat, toe_sqrt_nJ_sat_thr, false);
475 |
476 | // Compress the colorfulness. The goal is to compress less saturated colors more and
477 | // more saturated colors less, especially in the highlights. This step creates the
478 | // saturation roll-off in the highlights, but attemps to preserve pure colors. This
479 | // mostly affects highlights and mid-tones, and does not compress shadows.
480 | M_compr = toe(M_compr, limit, toe_nJ_compr, snJ, false);
481 |
482 | // Denormalize
483 | M_compr = M_compr * Mnorm;
484 | }
485 |
486 | float out[3] = {tonemapped_J, M_compr, h};
487 | return out;
488 | }
489 |
490 | float[3] chroma_compress_inv(float JMh[3],
491 | float J,
492 | ODTParams p,
493 | bool invert = false)
494 | {
495 | float tonemapped_J = JMh[0];
496 | float M_compr = JMh[1];
497 | float h = JMh[2];
498 |
499 | float M = M_compr;
500 |
501 | if (M_compr != 0.0)
502 | {
503 | const float nJ = tonemapped_J / p.limit_J_max;
504 | const float snJ = max(0., 1. - nJ);
505 | float Mnorm = chroma_compress_norm(h, p.chroma_compress_scale);
506 | float limit = pow(nJ, p.model_gamma_inv) * reach_M_from_table(h, p.TABLE_reach_M) / Mnorm;
507 |
508 | float toe_limit = limit - 0.001;
509 | float toe_snJ_sat = snJ * p.sat;
510 | float toe_sqrt_nJ_sat_thr = sqrt(nJ * nJ + p.sat_thr);
511 | float toe_nJ_compr = nJ * p.compr;
512 |
513 | M = M_compr / Mnorm;
514 | M = toe(M, limit, toe_nJ_compr, snJ, true);
515 | M = limit - toe(limit - M, toe_limit, toe_snJ_sat, toe_sqrt_nJ_sat_thr, true);
516 | M = M * Mnorm;
517 | M = M * pow(tonemapped_J / J, -p.model_gamma_inv);
518 | }
519 |
520 | float out[3] = {J, M, h};
521 | return out;
522 | }
523 |
524 | float[3] tonemap_and_compress_fwd(float JMh[3],
525 | ODTParams p)
526 | {
527 | // Applies the forward tonescale, then compresses M based on J and tonemapped J
528 |
529 | // Tonemap
530 | float linear = J_to_Y(JMh[0], p.input_params) / ref_luminance;
531 |
532 | float tonemapped_Y = tonescale_fwd(linear, p.ts);
533 |
534 | float J_ts = Y_to_J(tonemapped_Y, p.input_params);
535 |
536 | // Compress M; function returns { tonemapped J, compressed M, h }
537 | float JMh_tc[3] = chroma_compress_fwd(JMh, J_ts, p, false);
538 |
539 | return JMh_tc;
540 | }
541 |
542 | float[3] tonemap_and_compress_inv(float JMh_tc[3],
543 | ODTParams p)
544 | {
545 | // Applies the inverse tonescale, then uncompresses M based on tonemapped J and J
546 |
547 | // Un-tonemap
548 | float luminance = J_to_Y(JMh_tc[0], p.input_params);
549 |
550 | float linear = tonescale_inv(luminance / ref_luminance, p.ts);
551 |
552 | float J = Y_to_J(linear * ref_luminance, p.input_params);
553 |
554 | // Un-compress M; function returns { J, M, h }
555 | float JMh[3] = chroma_compress_inv(JMh_tc, J, p, true);
556 |
557 | return JMh;
558 | }
559 |
560 | float compute_compression_vector_slope(float intersect_J,
561 | float focus_J,
562 | float limit_J_max,
563 | float slope_gain)
564 | {
565 | float direction_scalar;
566 | if (intersect_J < focus_J)
567 | {
568 | direction_scalar = intersect_J;
569 | }
570 | else
571 | {
572 | direction_scalar = limit_J_max - intersect_J;
573 | }
574 | return direction_scalar * (intersect_J - focus_J) / (focus_J * slope_gain);
575 | }
576 |
577 | float solve_J_intersect(float J,
578 | float M,
579 | float focusJ,
580 | float maxJ,
581 | float slope_gain)
582 | {
583 | const float M_scaled = M / slope_gain;
584 | const float a = M_scaled / focusJ;
585 |
586 | if (J < focusJ)
587 | {
588 | const float b = 1. - M_scaled;
589 | const float c = -J;
590 | const float det = b * b - 4. * a * c;
591 | const float root = sqrt(det);
592 | return -2. * c / (b + root);
593 | }
594 | else
595 | {
596 | const float b = -(1. + M_scaled + maxJ * a);
597 | const float c = maxJ * M_scaled + J;
598 | const float det = b * b - 4. * a * c;
599 | const float root = sqrt(det);
600 | return -2. * c / (b - root);
601 | }
602 | }
603 |
604 | // Smooth minimum about the scaled reference, based upon a cubic polynomial
605 | float smin_scaled(float a,
606 | float b,
607 | float scale_reference)
608 | {
609 | const float s_scaled = smooth_cusps * scale_reference;
610 | const float h = max(s_scaled - fabs(a - b), 0.0) / s_scaled;
611 | return min(a, b) - h * h * h * s_scaled * (1. / 6.);
612 | }
613 |
614 | float estimate_line_and_boundary_intersection_M(float J_axis_intersect,
615 | float slope,
616 | float inv_gamma,
617 | float J_max,
618 | float M_max,
619 | float J_intersection_reference)
620 | {
621 | // Line defined by J = slope * x + J_axis_intersect
622 | // Boundary defined by J = J_max * (x / M_max) ^ (1/inv_gamma)
623 | // Approximate as we do not want to iteratively solve intersection of a
624 | // straight line and an exponential
625 |
626 | // We calculate a shifted intersection from the original intersection using
627 | // the inverse of the exponential and the provided reference
628 | const float normalised_J = J_axis_intersect / J_intersection_reference;
629 | const float shifted_intersection = J_intersection_reference * pow(normalised_J, inv_gamma);
630 |
631 | // Now we find the M intersection of two lines
632 | // line from origin to J,M Max l1(x) = J/M * x
633 | // line from J Intersect' with slope l2(x) = slope * x + Intersect'
634 |
635 | // return shifted_intersection / ((J_max / M_max) - slope);
636 | return shifted_intersection * M_max / (J_max - slope * M_max);
637 | }
638 |
639 | float find_gamut_boundary_intersection(float JM_cusp[2],
640 | float J_max,
641 | float gamma_top_inv,
642 | float gamma_bottom_inv,
643 | float J_intersect_source,
644 | float slope,
645 | float J_intersect_cusp)
646 | {
647 | const float M_boundary_lower = estimate_line_and_boundary_intersection_M(J_intersect_source,
648 | slope,
649 | gamma_bottom_inv,
650 | JM_cusp[0],
651 | JM_cusp[1],
652 | J_intersect_cusp);
653 |
654 | // The upper hull is flipped and thus 'zeroed' at J_max
655 | // Also note we negate the slope
656 | const float f_J_intersect_cusp = J_max - J_intersect_cusp;
657 | const float f_J_intersect_source = J_max - J_intersect_source;
658 | const float f_JM_cusp_J = J_max - JM_cusp[0];
659 | const float M_boundary_upper = estimate_line_and_boundary_intersection_M(f_J_intersect_source,
660 | -slope,
661 | gamma_top_inv,
662 | f_JM_cusp_J,
663 | JM_cusp[1],
664 | f_J_intersect_cusp);
665 |
666 | // Smooth minimum between the two calculated values for the M component
667 | float M_boundary = smin_scaled(M_boundary_lower, M_boundary_upper, JM_cusp[1]);
668 | return M_boundary;
669 | }
670 |
671 | float hueDependentUpperHullGamma(float h,
672 | float gamma_table[])
673 | {
674 | const int i_lo = hue_position_in_uniform_table(h, tableSize) + baseIndex;
675 | const int i_hi = next_position_in_table(i_lo, gamma_table.size);
676 |
677 | const float base_hue = base_hue_for_position(i_lo - baseIndex, tableSize);
678 |
679 | const float t = wrap_to_360(h) - base_hue;
680 |
681 | return lerp(gamma_table[i_lo], gamma_table[i_hi], t);
682 | }
683 |
684 | float get_focus_gain(float J,
685 | float analytical_threshold,
686 | float limit_J_max,
687 | float focus_dist)
688 | {
689 | float gain = limit_J_max * focus_dist;
690 |
691 | if (J > analytical_threshold)
692 | {
693 | // Approximate inverse required above threshold due to the introduction of J in the calculation
694 | float gain_adjustment = log10((limit_J_max - analytical_threshold) / max(0.0001, limit_J_max - J));
695 | gain_adjustment = gain_adjustment * gain_adjustment + 1.;
696 | gain = gain * gain_adjustment;
697 | }
698 |
699 | return gain;
700 | }
701 |
702 | float remap_M(float M,
703 | float gamut_boundary_M,
704 | float reach_boundary_M,
705 | bool invert = false)
706 | {
707 | const float boundary_ratio = gamut_boundary_M / reach_boundary_M;
708 | const float proportion = max(boundary_ratio, compression_threshold);
709 | const float threshold = proportion * gamut_boundary_M;
710 |
711 | if (M <= threshold || proportion >= 1.)
712 | return M;
713 |
714 | // Translate to place threshold at zero
715 | const float m_offset = M - threshold;
716 | const float gamut_offset = gamut_boundary_M - threshold;
717 | const float reach_offset = reach_boundary_M - threshold;
718 |
719 | const float scale = reach_offset / ((reach_offset / gamut_offset) - 1.);
720 | const float nd = m_offset / scale;
721 |
722 | // Shift result back to absolute
723 | return threshold + reinhard_remap(scale, nd, invert);
724 | }
725 |
726 | float[3] compress_gamut(float JMh[3],
727 | float Jx,
728 | ODTParams p,
729 | HueDependentGamutParams hdp,
730 | bool invert = false)
731 | {
732 | const float J = JMh[0];
733 | const float M = JMh[1];
734 | const float h = JMh[2];
735 |
736 | const float slope_gain = get_focus_gain(Jx, hdp.analytical_threshold, p.limit_J_max, p.focus_dist);
737 | const float J_intersect_source = solve_J_intersect(J, M, hdp.focus_J, p.limit_J_max, slope_gain);
738 | const float gamut_slope = compute_compression_vector_slope(J_intersect_source, hdp.focus_J, p.limit_J_max, slope_gain);
739 |
740 | const float J_intersect_cusp = solve_J_intersect(hdp.JMcusp[0], hdp.JMcusp[1], hdp.focus_J, p.limit_J_max, slope_gain);
741 |
742 | const float gamut_boundary_M = find_gamut_boundary_intersection(hdp.JMcusp,
743 | p.limit_J_max,
744 | hdp.gamma_top_inv,
745 | hdp.gamma_bottom_inv,
746 | J_intersect_source,
747 | gamut_slope,
748 | J_intersect_cusp);
749 |
750 | if (gamut_boundary_M <= 0.)
751 | {
752 | float returnJMh[3] = {J, 0., h};
753 | return returnJMh;
754 | }
755 |
756 | float reach_max_M = reach_M_from_table(h, p.TABLE_reach_M);
757 |
758 | const float reach_boundary_M = estimate_line_and_boundary_intersection_M(J_intersect_source,
759 | gamut_slope,
760 | p.model_gamma_inv,
761 | p.limit_J_max,
762 | reach_max_M,
763 | p.limit_J_max);
764 |
765 | const float remapped_M = remap_M(M, gamut_boundary_M, reach_boundary_M, invert);
766 |
767 | float JMhcompressed[3] = {J_intersect_source + remapped_M * gamut_slope,
768 | remapped_M,
769 | h};
770 |
771 | return JMhcompressed;
772 | }
773 |
774 | float[2] cusp_from_table(float h,
775 | float table[][3])
776 | {
777 | float lo[3];
778 | float hi[3];
779 |
780 | int low_i = 0;
781 | int high_i = baseIndex + tableSize;
782 | int i = hue_position_in_uniform_table(h, tableSize) + baseIndex;
783 |
784 | while (low_i + 1 < high_i)
785 | {
786 | if (h > table[i][2])
787 | {
788 | low_i = i;
789 | }
790 | else
791 | {
792 | high_i = i;
793 | }
794 | i = midpoint(low_i, high_i);
795 | }
796 | lo = table[high_i - 1];
797 | hi = table[high_i];
798 |
799 | float t = (h - lo[2]) / (hi[2] - lo[2]);
800 | float cusp_J = lerp(lo[0], hi[0], t);
801 | float cusp_M = lerp(lo[1], hi[1], t);
802 |
803 | float cusp_JM[2] = {cusp_J, cusp_M};
804 |
805 | return cusp_JM;
806 | }
807 |
808 | int lookup_hue_interval(float h,
809 | float hue_table[totalTableSize],
810 | int hue_linearity_search_range[2])
811 | {
812 | // Search the given table for the interval containing the desired hue
813 | // Returns the upper index of the interval
814 |
815 | // We can narrow the search range based on the hues being almost uniform
816 | unsigned int i = baseIndex + hue_position_in_uniform_table(h, totalTableSize);
817 | unsigned int i_lo = max(baseIndex, i + hue_linearity_search_range[0]);
818 | unsigned int i_hi = min(baseIndex + tableSize, i + hue_linearity_search_range[1]);
819 |
820 | while (i_lo + 1 < i_hi)
821 | {
822 | if (h > hue_table[i])
823 | {
824 | i_lo = i;
825 | }
826 | else
827 | {
828 | i_hi = i;
829 | }
830 | i = midpoint(i_lo, i_hi);
831 | }
832 |
833 | i_hi = max(1, i_hi);
834 |
835 | return i_hi;
836 | }
837 |
838 | float interpolation_weight(float h, float h_lo, float h_hi)
839 | {
840 | return (h - h_lo);
841 | }
842 |
843 | float compute_focus_J(float cusp_J, float mid_J, float limit_J_max)
844 | {
845 | return lerp(cusp_J, mid_J, min(1, cusp_mid_blend - (cusp_J / limit_J_max)));
846 | }
847 |
848 | HueDependentGamutParams init_HueDependentGamutParams(float hue, ODTParams p)
849 | {
850 | HueDependentGamutParams hdp;
851 | hdp.gamma_bottom_inv = p.lower_hull_gamma_inv;
852 |
853 | const int i_hi = lookup_hue_interval(hue, p.TABLE_hues, p.hue_linearity_search_range);
854 | const float t = interpolation_weight(hue, p.TABLE_hues[i_hi - 1], p.TABLE_hues[i_hi]);
855 |
856 | hdp.JMcusp = cusp_from_table(hue, p.TABLE_gamut_cusps);
857 | hdp.gamma_top_inv = lerp(p.TABLE_upper_hull_gamma[i_hi - 1], p.TABLE_upper_hull_gamma[i_hi], t);
858 | hdp.focus_J = compute_focus_J(hdp.JMcusp[0], p.mid_J, p.limit_J_max);
859 | hdp.analytical_threshold = lerp(hdp.JMcusp[0], p.limit_J_max, focus_gain_blend);
860 |
861 | return hdp;
862 | }
863 |
864 | float[3] gamut_compress_fwd(float JMh[3],
865 | ODTParams p)
866 | {
867 | const float J = JMh[0];
868 | const float M = JMh[1];
869 | const float h = JMh[2];
870 |
871 | if (J <= 0.0)
872 | {
873 | float JMh[3] = {0., 0., h};
874 | return JMh;
875 | }
876 |
877 | if (M < 0. || J > p.limit_J_max)
878 | {
879 | float JMh[3] = {J, 0., h};
880 | return JMh;
881 | }
882 |
883 | HueDependentGamutParams hdp = init_HueDependentGamutParams(h, p);
884 |
885 | return compress_gamut(JMh, J, p, hdp, false);
886 | }
887 |
888 | float[3] gamut_compress_inv(float JMh[3],
889 | ODTParams p)
890 | {
891 | const float J = JMh[0];
892 | const float M = JMh[1];
893 | const float h = JMh[2];
894 |
895 | if (J <= 0.0)
896 | {
897 | float JMh[3] = {0., 0., h};
898 | return JMh;
899 | }
900 | if (M < 0. || J > p.limit_J_max)
901 | {
902 | float JMh[3] = {J, 0., h};
903 | return JMh;
904 | }
905 |
906 | HueDependentGamutParams hdp = init_HueDependentGamutParams(h, p);
907 |
908 | float Jx = J;
909 |
910 | if (Jx > hdp.analytical_threshold)
911 | {
912 | Jx = compress_gamut(JMh, Jx, p, hdp, true)[0];
913 | }
914 |
915 | return compress_gamut(JMh, Jx, p, hdp, true);
916 | }
917 |
918 | // Table building functions
919 | bool any_below_zero(float newLimitRGB[3])
920 | {
921 | return (newLimitRGB[0] < 0. || newLimitRGB[1] < 0. || newLimitRGB[2] < 0.);
922 | }
923 |
924 | JMhParams init_JMhParams(Chromaticities prims)
925 | {
926 | const Chromaticities CAM16_PRI = {
927 | {0.8336, 0.1735},
928 | {2.3854, -1.4659},
929 | {0.087, -0.125},
930 | {0.333, 0.333}};
931 |
932 | const float MATRIX_16[3][3] = XYZtoRGB_f33(CAM16_PRI, 1.0);
933 |
934 | const float base_cone_response_to_Aab[3][3] = {
935 | {2., 1., 1. / 9.},
936 | {1., -12. / 11., 1. / 9.},
937 | {1. / 20., 1. / 11., -2. / 9.}};
938 |
939 | const float RGB_TO_XYZ[3][3] = RGBtoXYZ_f33(prims, 1.0);
940 | const float XYZ_w[3] = mult_f3_f33(f3_from_f(ref_luminance), RGB_TO_XYZ);
941 |
942 | float Y_w = XYZ_w[1];
943 |
944 | // Step 0 - Converting CIE XYZ tristimulus values to sharpened RGB values
945 | float RGB_w[3] = mult_f3_f33(XYZ_w, MATRIX_16);
946 |
947 | // Viewing condition dependent parameters
948 | const float k = 1. / (5. * L_A + 1.);
949 | const float k4 = k * k * k * k;
950 | const float F_L = 0.2 * k4 * (5. * L_A) + 0.1 * pow((1. - k4), 2.) * pow(5. * L_A, 1. / 3.);
951 |
952 | const float F_L_n = F_L / ref_luminance;
953 | const float cz = model_gamma;
954 |
955 | const float D_RGB[3] = {
956 | F_L_n * Y_w / RGB_w[0],
957 | F_L_n * Y_w / RGB_w[1],
958 | F_L_n * Y_w / RGB_w[2]};
959 |
960 | const float RGB_wc[3] = {
961 | D_RGB[0] * RGB_w[0],
962 | D_RGB[1] * RGB_w[1],
963 | D_RGB[2] * RGB_w[2]};
964 |
965 | const float RGB_Aw[3] = {
966 | post_adaptation_cone_response_compression_fwd(RGB_wc[0]),
967 | post_adaptation_cone_response_compression_fwd(RGB_wc[1]),
968 | post_adaptation_cone_response_compression_fwd(RGB_wc[2])};
969 |
970 | float cone_response_to_Aab[3][3] = mult_f33_f33(mult_f_f33(cam_nl_scale, MATRIX_IDENTITY), base_cone_response_to_Aab);
971 | float A_w = cone_response_to_Aab[0][0] * RGB_Aw[0] + cone_response_to_Aab[1][0] * RGB_Aw[1] + cone_response_to_Aab[2][0] * RGB_Aw[2];
972 | float A_w_J = _post_adaptation_cone_response_compression_fwd(F_L);
973 |
974 | // Prescale the CAM16 LMS responses to directly provide for chromatic adaptation
975 | float M1[3][3] = mult_f33_f33(RGB_TO_XYZ, MATRIX_16);
976 | float M2[3][3] = mult_f_f33(ref_luminance, MATRIX_IDENTITY);
977 | float MATRIX_RGB_to_CAM16[3][3] = mult_f33_f33(M1, M2);
978 | float MATRIX_RGB_to_CAM16_c[3][3] = mult_f33_f33(MATRIX_RGB_to_CAM16, scale_matrix_diagonal_f33_f3(MATRIX_IDENTITY, D_RGB));
979 |
980 | float MATRIX_cone_response_to_Aab[3][3] = {
981 | {cone_response_to_Aab[0][0] / A_w, cone_response_to_Aab[0][1] * 43. * surround[2], cone_response_to_Aab[0][2] * 43. * surround[2]},
982 | {cone_response_to_Aab[1][0] / A_w, cone_response_to_Aab[1][1] * 43. * surround[2], cone_response_to_Aab[1][2] * 43. * surround[2]},
983 | {cone_response_to_Aab[2][0] / A_w, cone_response_to_Aab[2][1] * 43. * surround[2], cone_response_to_Aab[2][2] * 43. * surround[2]}};
984 |
985 | JMhParams p;
986 | p.MATRIX_RGB_to_CAM16_c = MATRIX_RGB_to_CAM16_c;
987 | p.MATRIX_CAM16_c_to_RGB = invert_f33(MATRIX_RGB_to_CAM16_c);
988 | p.MATRIX_cone_response_to_Aab = MATRIX_cone_response_to_Aab;
989 | p.MATRIX_Aab_to_cone_response = invert_f33(MATRIX_cone_response_to_Aab);
990 | p.F_L_n = F_L_n;
991 | p.cz = cz;
992 | p.inv_cz = 1. / cz;
993 | p.A_w_J = A_w_J;
994 | p.inv_A_w_J = 1. / A_w_J;
995 |
996 | return p;
997 | }
998 |
999 | float[3] generate_unit_cube_cusp_corners(int corner)
1000 | {
1001 | float result[3];
1002 |
1003 | // Generation order R, Y, G, C, B, M to ensure hues rotate in correct order
1004 | if (((corner + 1) % cuspCornerCount) < 3)
1005 | result[0] = 1;
1006 | else
1007 | result[0] = 0;
1008 | if (((corner + 5) % cuspCornerCount) < 3)
1009 | result[1] = 1;
1010 | else
1011 | result[1] = 0;
1012 | if (((corner + 3) % cuspCornerCount) < 3)
1013 | result[2] = 1;
1014 | else
1015 | result[2] = 0;
1016 |
1017 | return result;
1018 | }
1019 |
1020 | void build_limiting_cusp_corners_tables(output float RGB_corners[totalCornerCount][3],
1021 | output float JMh_corners[totalCornerCount][3],
1022 | input JMhParams params,
1023 | input float peakLuminance)
1024 | {
1025 | // We calculate the RGB and JMh values for the limiting gamut cusp corners
1026 | // They are then arranged into a cycle with the lowest JMh value at [1] to
1027 | // allow for hue wrapping
1028 | float temp_RGB_corners[cuspCornerCount][3];
1029 | float temp_JMh_corners[cuspCornerCount][3];
1030 |
1031 | int min_index = 0;
1032 | for (int i = 0; i != cuspCornerCount; i = i + 1)
1033 | {
1034 | temp_RGB_corners[i] = mult_f_f3(peakLuminance / ref_luminance, generate_unit_cube_cusp_corners(i));
1035 | temp_JMh_corners[i] = RGB_to_JMh(temp_RGB_corners[i], params);
1036 | if (temp_JMh_corners[i][2] < temp_JMh_corners[min_index][2])
1037 | min_index = 1;
1038 | }
1039 |
1040 | // Rotate entries placing lowest at [1] (not [0])
1041 | for (int i = 0; i != cuspCornerCount; i = i + 1)
1042 | {
1043 | RGB_corners[i + 1] = temp_RGB_corners[(i + min_index) % cuspCornerCount];
1044 | JMh_corners[i + 1] = temp_JMh_corners[(i + min_index) % cuspCornerCount];
1045 | }
1046 |
1047 | // Copy end elements to create a cycle
1048 | RGB_corners[0] = RGB_corners[cuspCornerCount];
1049 | RGB_corners[cuspCornerCount + 1] = RGB_corners[1];
1050 | JMh_corners[0] = JMh_corners[cuspCornerCount];
1051 | JMh_corners[cuspCornerCount + 1] = JMh_corners[1];
1052 |
1053 | // Wrap the hues, to maintain monotonicity these entries will fall outside [0.0, hue_limit)
1054 | JMh_corners[0][2] = JMh_corners[0][2] - hue_limit;
1055 | JMh_corners[cuspCornerCount + 1][2] = JMh_corners[cuspCornerCount + 1][2] + hue_limit;
1056 |
1057 | // return JMh_corners;
1058 | }
1059 |
1060 | float[totalCornerCount][3] find_reach_corners_table(JMhParams params_reach,
1061 | ODTParams p)
1062 | {
1063 | // We need to find the value of JMh that corresponds to limitJ for each
1064 | // corner This is done by scaling the unit corners converting to JMh until
1065 | // the J value is near the limitJ
1066 | // As an optimisation we use the equivalent Achromatic value to search for
1067 | // the J value and avoid the non-linear transform during the search.
1068 | // Strictly speaking we should only need to find the R, G and B "corners"
1069 | // as the reach is unbounded and as such does not form a cube, but is formed
1070 | // by the transformed 3 lower planes of the cube and the plane at J = limitJ
1071 | float temp_JMh_corners[cuspCornerCount][3];
1072 |
1073 | float JMh_corners[totalCornerCount][3];
1074 |
1075 | float limitA = J_to_Achromatic_n(p.limit_J_max, params_reach.inv_cz);
1076 |
1077 | int min_index = 0;
1078 | for (int i = 0; i != cuspCornerCount; i = i + 1)
1079 | {
1080 | const float rgb_vector[3] = generate_unit_cube_cusp_corners(i);
1081 |
1082 | float lower = 0.0;
1083 | float upper = p.ts.forward_limit;
1084 |
1085 | while ((upper - lower) > reach_cusp_tolerance)
1086 | {
1087 | float test = (lower + upper) / 2.;
1088 | float test_corner[3] = mult_f_f3(test, rgb_vector);
1089 | float A = RGB_to_Aab(test_corner, params_reach)[0];
1090 | if (A < limitA)
1091 | {
1092 | lower = test;
1093 | }
1094 | else
1095 | {
1096 | upper = test;
1097 | }
1098 | }
1099 |
1100 | temp_JMh_corners[i] = RGB_to_JMh(mult_f_f3(upper, rgb_vector), params_reach);
1101 |
1102 | if (temp_JMh_corners[i][2] < temp_JMh_corners[min_index][2])
1103 | min_index = i;
1104 | }
1105 |
1106 | // Rotate entries placing lowest at [1] (not [0])
1107 | for (int i = 0; i != cuspCornerCount; i = i + 1)
1108 | {
1109 | JMh_corners[i + 1] = temp_JMh_corners[(i + min_index) % cuspCornerCount];
1110 | }
1111 |
1112 | // Copy end elements to create a cycle
1113 | JMh_corners[0] = JMh_corners[cuspCornerCount];
1114 | JMh_corners[cuspCornerCount + 1] = JMh_corners[1];
1115 |
1116 | // Wrap the hues, to maintain monotonicity these entries will fall outside [0.0, hue_limit)
1117 | JMh_corners[0][2] = JMh_corners[0][2] - hue_limit;
1118 | JMh_corners[cuspCornerCount + 1][2] = JMh_corners[cuspCornerCount + 1][2] + hue_limit;
1119 |
1120 | return JMh_corners;
1121 | }
1122 |
1123 | float[max_sorted_corners] extract_sorted_cube_hues(float reach_JMh[totalCornerCount][3],
1124 | float limit_JMh[totalCornerCount][3])
1125 | {
1126 | float sorted_hues[max_sorted_corners];
1127 |
1128 | // Basic merge of 2 sorted arrays, extracting the unique hues.
1129 | // Return the count of the unique hues
1130 | int idx = 0;
1131 | int reach_idx = 1;
1132 | int limit_idx = 1;
1133 | while ((reach_idx < (cuspCornerCount + 1)) || (limit_idx < (cuspCornerCount + 1)))
1134 | {
1135 | float reach_hue = reach_JMh[reach_idx][2];
1136 | float limit_hue = limit_JMh[limit_idx][2];
1137 | if (reach_hue == limit_hue)
1138 | {
1139 | sorted_hues[idx] = reach_hue;
1140 | reach_idx = reach_idx + 1;
1141 | limit_idx = limit_idx + 1; // When equal consume both
1142 | }
1143 | else
1144 | {
1145 | if (reach_hue < limit_hue)
1146 | {
1147 | sorted_hues[idx] = reach_hue;
1148 | reach_idx = reach_idx + 1;
1149 | }
1150 | else
1151 | {
1152 | sorted_hues[idx] = limit_hue;
1153 | limit_idx = limit_idx + 1;
1154 | }
1155 | }
1156 | idx = idx + 1;
1157 | }
1158 | return sorted_hues;
1159 | }
1160 |
1161 | float[totalTableSize] build_hue_sample_interval(int samples,
1162 | float lower,
1163 | float upper,
1164 | float hue_table[totalTableSize],
1165 | int base)
1166 | {
1167 | float mod_hue_table[totalTableSize] = hue_table;
1168 | float delta = (upper - lower) / samples;
1169 | int i;
1170 | for (i = 0; i != samples; i = i + 1)
1171 | {
1172 | mod_hue_table[base + i] = lower + i * delta;
1173 | }
1174 |
1175 | return mod_hue_table;
1176 | }
1177 |
1178 | float[totalTableSize] build_hue_table(float sorted_hues[max_sorted_corners])
1179 | {
1180 | float hue_table[totalTableSize];
1181 |
1182 | float ideal_spacing = tableSize / hue_limit;
1183 | int samples_count[2 * cuspCornerCount + 2];
1184 | int last_idx;
1185 | int min_index;
1186 | if (sorted_hues[0] == 0.0)
1187 | {
1188 | min_index = 0;
1189 | }
1190 | else
1191 | {
1192 | min_index = 1;
1193 | }
1194 | int hue_idx;
1195 |
1196 | for (hue_idx = 0; hue_idx != max_sorted_corners; hue_idx = hue_idx + 1)
1197 | {
1198 | float raw_idx = round(sorted_hues[hue_idx] * ideal_spacing);
1199 | int nominal_idx = min(max(round(sorted_hues[hue_idx] * ideal_spacing), min_index), tableSize - 1);
1200 |
1201 | if (last_idx == nominal_idx)
1202 | {
1203 | // Last two hues should sample at same index, need to adjust them
1204 | // Adjust previous sample down if we can
1205 | if (hue_idx > 1 && samples_count[hue_idx - 2] != (samples_count[hue_idx - 1] - 1))
1206 | {
1207 | samples_count[hue_idx - 1] = samples_count[hue_idx - 1] - 1;
1208 | }
1209 | else
1210 | {
1211 | nominal_idx = nominal_idx + 1;
1212 | }
1213 | }
1214 | samples_count[hue_idx] = min(nominal_idx, tableSize - 1);
1215 | min_index = nominal_idx;
1216 | last_idx = min_index;
1217 | }
1218 |
1219 | int total_samples = 0;
1220 | // Special cases for ends
1221 | int i = 0;
1222 | hue_table = build_hue_sample_interval(samples_count[i], 0.0, sorted_hues[i], hue_table, total_samples + 1);
1223 | total_samples = total_samples + samples_count[i];
1224 |
1225 | for (i = i + 1; i != max_sorted_corners; i = i + 1)
1226 | {
1227 | int samples = samples_count[i] - samples_count[i - 1];
1228 | hue_table = build_hue_sample_interval(samples, sorted_hues[i - 1], sorted_hues[i], hue_table, total_samples + 1);
1229 | total_samples = total_samples + samples;
1230 | }
1231 | hue_table = build_hue_sample_interval(tableSize - total_samples, sorted_hues[i - 1], hue_limit, hue_table, total_samples + 1);
1232 |
1233 | hue_table[0] = hue_table[baseIndex + tableSize - 1] - hue_limit;
1234 | hue_table[baseIndex + tableSize] = hue_table[baseIndex] + hue_limit;
1235 |
1236 | return hue_table;
1237 | }
1238 |
1239 | float[2] find_display_cusp_for_hue(float hue,
1240 | float RGB_corners[totalCornerCount][3],
1241 | float JMh_corners[totalCornerCount][3],
1242 | JMhParams params,
1243 | float previous[2])
1244 | {
1245 | // This works by finding the required line segment between two of the XYZ
1246 | // cusp corners, then binary searching along the line calculating the JMh of
1247 | // points along the line till we find the required value. All values on the
1248 | // line segments are valid cusp locations.
1249 | float return_JM[2];
1250 |
1251 | int upper_corner = 1;
1252 | int found = 0;
1253 | for (int i = upper_corner; i != totalCornerCount && !found; i = i + 1)
1254 | {
1255 | if (JMh_corners[i][2] > hue)
1256 | {
1257 | upper_corner = i;
1258 | found = 1;
1259 | }
1260 | }
1261 | int lower_corner = upper_corner - 1;
1262 |
1263 | // hue should now be within [lower_corner, upper_corner), handle exact match
1264 | if (JMh_corners[lower_corner][2] == hue)
1265 | {
1266 | return_JM[0] = JMh_corners[lower_corner][0];
1267 | return_JM[1] = JMh_corners[lower_corner][1];
1268 | return return_JM;
1269 | }
1270 |
1271 | // search by lerping between RGB corners for the hue
1272 | float cusp_lower[3] = RGB_corners[lower_corner];
1273 | float cusp_upper[3] = RGB_corners[upper_corner];
1274 | float sample[3];
1275 |
1276 | float sample_t;
1277 | float lower_t = 0.0;
1278 | if (upper_corner == previous[0])
1279 | lower_t = previous[1];
1280 | float upper_t = 1.0;
1281 |
1282 | float JMh[3];
1283 |
1284 | // There is an edge case where we need to search towards the range when
1285 | // across the [0.0, hue_limit] boundary each edge needs the directions
1286 | // swapped. This is handled by comparing against the appropriate corner to
1287 | // make sure we are still in the expected range between the lower and upper
1288 | // corner hue limits
1289 | while ((upper_t - lower_t) > display_cusp_tolerance)
1290 | {
1291 | sample_t = midpoint(lower_t, upper_t);
1292 | sample = lerp_f3(cusp_lower, cusp_upper, sample_t);
1293 | JMh = RGB_to_JMh(sample, params);
1294 | if (JMh[2] < JMh_corners[lower_corner][2])
1295 | {
1296 | upper_t = sample_t;
1297 | }
1298 | else if (JMh[2] >= JMh_corners[upper_corner][2])
1299 | {
1300 | lower_t = sample_t;
1301 | }
1302 | else if (JMh[2] > hue)
1303 | {
1304 | upper_t = sample_t;
1305 | }
1306 | else
1307 | {
1308 | lower_t = sample_t;
1309 | }
1310 | }
1311 |
1312 | // Use the midpoint of the final interval for the actual samples
1313 | sample_t = midpoint(lower_t, upper_t);
1314 | sample = lerp_f3(cusp_lower, cusp_upper, sample_t);
1315 | JMh = RGB_to_JMh(sample, params);
1316 |
1317 | // previous[0] = upper_corner;
1318 | // previous[1] = sample_t;
1319 |
1320 | return_JM[0] = JMh[0];
1321 | return_JM[1] = JMh[1];
1322 | return return_JM;
1323 | }
1324 |
1325 | float[totalTableSize][3] build_cusp_table(float hue_table[totalTableSize],
1326 | float RGB_corners[totalCornerCount][3],
1327 | float JMh_corners[totalCornerCount][3],
1328 | JMhParams params)
1329 | {
1330 | float previous[2] = {0.0, 0.0};
1331 | float output_table[totalTableSize][3];
1332 |
1333 | for (int i = baseIndex; i != totalTableSize; i = i + 1)
1334 | {
1335 | float hue = hue_table[i];
1336 | float JM[2] = find_display_cusp_for_hue(hue, RGB_corners, JMh_corners, params, previous);
1337 | output_table[i][0] = JM[0];
1338 | output_table[i][1] = JM[1] * (1. + smooth_m * smooth_cusps);
1339 | output_table[i][2] = hue;
1340 | }
1341 |
1342 | // Copy last nominal entry to start
1343 | output_table[0][0] = output_table[tableSize][0];
1344 | output_table[0][1] = output_table[tableSize][1];
1345 | output_table[0][2] = hue_table[0];
1346 |
1347 | // Copy first nominal entry to end
1348 | output_table[baseIndex + tableSize][0] = output_table[baseIndex][0];
1349 | output_table[baseIndex + tableSize][1] = output_table[baseIndex][1];
1350 | output_table[baseIndex + tableSize][2] = hue_table[baseIndex + tableSize];
1351 |
1352 | return output_table;
1353 | }
1354 |
1355 | float[totalTableSize][3] make_uniform_hue_gamut_table(JMhParams reach_params,
1356 | JMhParams limit_params,
1357 | ODTParams p)
1358 | {
1359 | // The principal here is to sample the hues as uniformly as possible, whilst
1360 | // ensuring we sample the corners of the limiting gamut and the reach
1361 | // primaries at limit J Max
1362 | //
1363 | // The corners are calculated then the hues are extracted and merged to form
1364 | // a unique sorted hue list We then build the hue table from the list, those
1365 | // hues are then used to compute the JMh of the limiting gamut cusp.
1366 |
1367 | float reach_JMh_corners[totalCornerCount][3];
1368 | float limiting_RGB_corners[totalCornerCount][3];
1369 | float limiting_JMh_corners[totalCornerCount][3];
1370 |
1371 | reach_JMh_corners = find_reach_corners_table(reach_params, p);
1372 | build_limiting_cusp_corners_tables(limiting_RGB_corners, limiting_JMh_corners, limit_params, p.peakLuminance);
1373 | float sorted_hues[max_sorted_corners] = extract_sorted_cube_hues(reach_JMh_corners,
1374 | limiting_JMh_corners);
1375 |
1376 | float hue_table[totalTableSize] = build_hue_table(sorted_hues);
1377 |
1378 | float cusp_JMh_table[totalTableSize][3] = build_cusp_table(hue_table, limiting_RGB_corners, limiting_JMh_corners, limit_params);
1379 |
1380 | return cusp_JMh_table;
1381 | }
1382 |
1383 | // Finds reach gamut M value at limitJmax
1384 | float[totalTableSize] make_reach_m_table(JMhParams params,
1385 | float limitJmax)
1386 | {
1387 | float reachTable[totalTableSize];
1388 |
1389 | for (int i = 0; i < tableSize; i = i + 1)
1390 | {
1391 | float i_float = i;
1392 | float hue = base_hue_for_position(i, tableSize);
1393 |
1394 | const float search_range = 50.;
1395 | const float search_maximum = 1300.;
1396 | float low = 0.;
1397 | float high = low + search_range;
1398 | bool outside = false;
1399 |
1400 | while ((outside != true) & (high < search_maximum))
1401 | {
1402 | float searchJMh[3] = {limitJmax, high, hue};
1403 | float newLimitRGB[3] = JMh_to_RGB(searchJMh, params);
1404 | outside = any_below_zero(newLimitRGB);
1405 | if (outside == false)
1406 | {
1407 | low = high;
1408 | high = high + search_range;
1409 | }
1410 | }
1411 |
1412 | while (high - low > 1e-2)
1413 | {
1414 | float sampleM = (high + low) / 2.;
1415 | float searchJMh[3] = {limitJmax, sampleM, hue};
1416 | float newLimitRGB[3] = JMh_to_RGB(searchJMh, params);
1417 | outside = any_below_zero(newLimitRGB);
1418 | if (outside)
1419 | {
1420 | high = sampleM;
1421 | }
1422 | else
1423 | {
1424 | low = sampleM;
1425 | }
1426 | }
1427 |
1428 | reachTable[i + baseIndex] = high;
1429 | }
1430 |
1431 | // Copy last populated entry to first empty spot
1432 | reachTable[0] = reachTable[tableSize];
1433 |
1434 | // Copy first populated entry to last empty spot
1435 | reachTable[baseIndex + tableSize] = reachTable[baseIndex];
1436 |
1437 | return reachTable;
1438 | }
1439 |
1440 | bool outside_hull(float rgb[3], float maxRGBtestVal)
1441 | {
1442 | return rgb[0] > maxRGBtestVal || rgb[1] > maxRGBtestVal || rgb[2] > maxRGBtestVal;
1443 | }
1444 |
1445 | const int test_count = 5;
1446 | const float testPositions[test_count] = {0.01, 0.1, 0.5, 0.8, 0.99};
1447 |
1448 | struct TestData
1449 | {
1450 | float test_JMh[3];
1451 | float J_intersect_source;
1452 | float slope;
1453 | float J_intersect_cusp;
1454 | };
1455 |
1456 | void generate_gamma_test_data(input float JMcusp[2],
1457 | input float hue,
1458 | input float limit_J_max,
1459 | input float mid_J,
1460 | input float focus_dist,
1461 | output float test_JMh[test_count][3],
1462 | output float J_intersect_source[test_count],
1463 | output float slopes[test_count],
1464 | output float J_intersect_cusp[test_count])
1465 | {
1466 | float analytical_threshold = lerp(JMcusp[0], limit_J_max, focus_gain_blend);
1467 | float focus_J = compute_focus_J(JMcusp[0], mid_J, limit_J_max);
1468 |
1469 | for (int testIndex = 0; testIndex != test_count; testIndex = testIndex + 1)
1470 | {
1471 | float test_J = lerp(JMcusp[0], limit_J_max, testPositions[testIndex]);
1472 | float slope_gain = get_focus_gain(test_J, analytical_threshold, limit_J_max, focus_dist);
1473 | float J_intersect = solve_J_intersect(test_J, JMcusp[1], focus_J, limit_J_max, slope_gain);
1474 | float slope = compute_compression_vector_slope(J_intersect, focus_J, limit_J_max, slope_gain);
1475 | float J_cusp = solve_J_intersect(JMcusp[0], JMcusp[1], focus_J, limit_J_max, slope_gain);
1476 |
1477 | // Store values in parallel arrays
1478 | test_JMh[testIndex][0] = test_J;
1479 | test_JMh[testIndex][1] = JMcusp[1];
1480 | test_JMh[testIndex][2] = hue;
1481 | J_intersect_source[testIndex] = J_intersect;
1482 | slopes[testIndex] = slope;
1483 | J_intersect_cusp[testIndex] = J_cusp;
1484 | }
1485 | }
1486 |
1487 | bool evaluate_gamma_fit(float JMcusp[2],
1488 | float test_JMh[test_count][3],
1489 | float J_intersect_source[test_count],
1490 | float slopes[test_count],
1491 | float J_intersect_cusp[test_count],
1492 | float top_gamma_inv,
1493 | float peakLuminance,
1494 | float limit_J_max,
1495 | float lower_hull_gamma_inv,
1496 | JMhParams limit_params)
1497 | {
1498 | float luminance_limit = peakLuminance / ref_luminance;
1499 |
1500 | for (int testIndex = 0; testIndex < test_count; testIndex = testIndex + 1)
1501 | {
1502 | // Compute gamut boundary intersection
1503 | float approxLimit_M = find_gamut_boundary_intersection(JMcusp,
1504 | limit_J_max,
1505 | top_gamma_inv,
1506 | lower_hull_gamma_inv,
1507 | J_intersect_source[testIndex],
1508 | slopes[testIndex],
1509 | J_intersect_cusp[testIndex]);
1510 | float approxLimit_J = J_intersect_source[testIndex] + slopes[testIndex] * approxLimit_M;
1511 |
1512 | // Store JMh values
1513 | float approximate_JMh[3] = {approxLimit_J, approxLimit_M, test_JMh[testIndex][2]};
1514 |
1515 | // Convert to RGB
1516 | float newLimitRGB[3] = JMh_to_RGB(approximate_JMh, limit_params);
1517 |
1518 | // Check if any values exceed the luminance limit. If so, we are outside of the top gamut shell.
1519 | if (!outside_hull(newLimitRGB, luminance_limit))
1520 | return false;
1521 | }
1522 |
1523 | return true;
1524 | }
1525 |
1526 | float[totalTableSize] make_upper_hull_gamma_table(float gamutCuspTable[totalTableSize][3],
1527 | ODTParams p)
1528 | {
1529 | // Find upper hull gamma values for the gamut mapper.
1530 | // Start by taking a h angle
1531 | // Get the cusp J value for that angle
1532 | // Find a J value halfway to the Jmax
1533 | // Iterate through gamma values until the approximate max M is
1534 | // negative through the actual boundary
1535 |
1536 | // positions between the cusp and Jmax we will check variables that get
1537 | // set as we iterate through, once all are set to true we break the loop
1538 |
1539 | float upper_hull_gamma[totalTableSize];
1540 |
1541 | for (int i = baseIndex; i != baseIndex + tableSize; i = i + 1)
1542 | {
1543 | // Get cusp from cusp table at hue position
1544 | float hue = gamutCuspTable[i][2];
1545 | float JMcusp[2] = {gamutCuspTable[i][0], gamutCuspTable[i][1]};
1546 |
1547 | float test_JMh[test_count][3];
1548 | float J_intersect_source[test_count];
1549 | float slopes[test_count];
1550 | float J_intersect_cusp[test_count];
1551 |
1552 | generate_gamma_test_data(JMcusp, hue, p.limit_J_max, p.mid_J, p.focus_dist,
1553 | test_JMh, J_intersect_source, slopes, J_intersect_cusp);
1554 |
1555 | float search_range = gamma_search_step;
1556 | float low = gamma_minimum;
1557 | float high = low + search_range;
1558 | bool outside = false;
1559 | while (!(outside) && (high < gamma_maximum))
1560 | {
1561 | bool gammaFound = evaluate_gamma_fit(JMcusp,
1562 | test_JMh, J_intersect_source, slopes, J_intersect_cusp,
1563 | 1. / high,
1564 | p.peakLuminance, p.limit_J_max, p.lower_hull_gamma_inv, p.limit_params);
1565 | if (!gammaFound)
1566 | {
1567 | low = high;
1568 | high = high + search_range;
1569 | }
1570 | else
1571 | {
1572 | outside = true;
1573 | }
1574 | }
1575 |
1576 | float testGamma = -1.0;
1577 | while ((high - low) > gamma_accuracy)
1578 | {
1579 | testGamma = midpoint(high, low);
1580 | bool gammaFound = evaluate_gamma_fit(JMcusp,
1581 | test_JMh, J_intersect_source, slopes, J_intersect_cusp,
1582 | 1. / testGamma,
1583 | p.peakLuminance, p.limit_J_max, p.lower_hull_gamma_inv, p.limit_params);
1584 | if (gammaFound)
1585 | {
1586 | high = testGamma;
1587 | }
1588 | else
1589 | {
1590 | low = testGamma;
1591 | }
1592 | }
1593 |
1594 | upper_hull_gamma[i] = 1. / high;
1595 | }
1596 |
1597 | // Copy last populated entry to first empty spot
1598 | upper_hull_gamma[0] = upper_hull_gamma[tableSize];
1599 |
1600 | // Copy first populated entry to last empty spot
1601 | upper_hull_gamma[tableSize + baseIndex] = upper_hull_gamma[baseIndex];
1602 |
1603 | return upper_hull_gamma;
1604 | }
1605 |
1606 | int[2] determine_hue_linearity_search_range(float hue_table[])
1607 | {
1608 | // This function searches through the hues looking for the largest
1609 | // deviations from a linear distribution. We can then use this to initialise
1610 | // the binary search range to something smaller than the full one to reduce
1611 | // the number of lookups per hue lookup from ~ceil(log2(table size)) to
1612 | // ~ceil(log2(range)) during image rendering.
1613 |
1614 | const int lower_padding = 0;
1615 | const int upper_padding = 1;
1616 |
1617 | int hue_linearity_search_range[2] = {lower_padding, upper_padding};
1618 |
1619 | for (int i = baseIndex; i != baseIndex + tableSize; i = i + 1)
1620 | {
1621 | const int pos = hue_position_in_uniform_table(hue_table[i], totalTableSize);
1622 | const int delta = i - pos;
1623 | hue_linearity_search_range[0] = min(hue_linearity_search_range[0], delta + lower_padding);
1624 | hue_linearity_search_range[1] = max(hue_linearity_search_range[1], delta + upper_padding);
1625 | }
1626 |
1627 | return hue_linearity_search_range;
1628 | }
1629 |
1630 | ODTParams init_ODTParams(float peakLuminance,
1631 | Chromaticities limitingPrimaries)
1632 | {
1633 | ODTParams p;
1634 |
1635 | p.peakLuminance = peakLuminance;
1636 |
1637 | // JMh parameters
1638 | p.input_params = init_JMhParams(AP0);
1639 | p.reach_params = init_JMhParams(REACH_PRI);
1640 | p.limit_params = init_JMhParams(limitingPrimaries);
1641 |
1642 | // Tonescale parameters
1643 | p.ts = init_TSParams(peakLuminance);
1644 |
1645 | // Shared compression paramters
1646 | p.limit_J_max = Y_to_J(peakLuminance, p.input_params);
1647 | p.model_gamma_inv = 1. / model_gamma;
1648 | p.TABLE_reach_M = make_reach_m_table(p.reach_params, p.limit_J_max);
1649 |
1650 | // Chroma compression parameters
1651 | p.sat = max(0.2, chroma_expand - (chroma_expand * chroma_expand_fact) * p.ts.log_peak);
1652 | p.sat_thr = chroma_expand_thr / peakLuminance;
1653 | p.compr = chroma_compress + (chroma_compress * chroma_compress_fact) * p.ts.log_peak;
1654 | p.chroma_compress_scale = pow(0.03379 * peakLuminance, 0.30596) - 0.45135;
1655 |
1656 | // Gamut compression parameters
1657 | p.mid_J = Y_to_J(p.ts.c_t * ref_luminance, p.input_params);
1658 | p.focus_dist = focus_distance + focus_distance * focus_distance_scaling * p.ts.log_peak;
1659 | const float lower_hull_gamma = 1.14 + 0.07 * p.ts.log_peak;
1660 | p.lower_hull_gamma_inv = 1. / lower_hull_gamma;
1661 | p.TABLE_gamut_cusps = make_uniform_hue_gamut_table(p.reach_params, p.limit_params, p);
1662 | for (int i = 0; i != totalTableSize; i = i + 1)
1663 | {
1664 | p.TABLE_hues[i] = p.TABLE_gamut_cusps[i][2];
1665 | }
1666 | p.TABLE_upper_hull_gamma = make_upper_hull_gamma_table(p.TABLE_gamut_cusps, p);
1667 | p.hue_linearity_search_range = determine_hue_linearity_search_range(p.TABLE_hues);
1668 |
1669 | return p;
1670 | }
1671 |
1672 | float[3] outputTransform_fwd(float aces[3],
1673 | ODTParams p)
1674 | {
1675 | float AP0_clamped[3] = clamp_AP0_to_AP1(aces, 0., p.ts.forward_limit);
1676 |
1677 | float JMh[3] = RGB_to_JMh(AP0_clamped, p.input_params);
1678 |
1679 | float tonemappedJMh[3] = tonemap_and_compress_fwd(JMh, p);
1680 |
1681 | float compressedJMh[3] = gamut_compress_fwd(tonemappedJMh, p);
1682 |
1683 | float RGBout[3] = JMh_to_RGB(compressedJMh, p.limit_params);
1684 |
1685 | return RGBout;
1686 | }
1687 |
1688 | float[3] outputTransform_inv(float RGBout[3],
1689 | ODTParams p)
1690 | {
1691 | float compressedJMh[3] = RGB_to_JMh(RGBout, p.limit_params);
1692 |
1693 | float tonemappedJMh[3] = gamut_compress_inv(compressedJMh, p);
1694 |
1695 | float JMh[3] = tonemap_and_compress_inv(tonemappedJMh, p);
1696 |
1697 | float aces[3] = JMh_to_RGB(JMh, p.input_params);
1698 |
1699 | return aces;
1700 | }
--------------------------------------------------------------------------------
/lib/Lib.Academy.Tonescale.ctl:
--------------------------------------------------------------------------------
1 |
2 | // urn:ampas:aces:transformId:v2.0:Lib.Academy.Tonescale.a2.v1
3 | // Tonescale Functions
4 |
5 | //
6 | // Library File with functions used for pre-calculating the tonescale parameters and
7 | // applying the fwd/inv tonescale function
8 | //
9 |
10 |
11 |
12 | struct TSParams
13 | {
14 | float n;
15 | float n_r;
16 | float g;
17 | float t_1;
18 | float c_t;
19 | float s_2;
20 | float u_2;
21 | float m_2;
22 | float forward_limit;
23 | float inverse_limit;
24 | float log_peak;
25 | };
26 |
27 | // Tonescale pre-calculations
28 | TSParams init_TSParams(float peakLuminance)
29 | {
30 |
31 | // Preset constants that set the desired behavior for the curve
32 | const float n = peakLuminance;
33 |
34 | const float n_r = 100.0; // normalized white in nits (what 1.0 should be)
35 | const float g = 1.15; // surround / contrast
36 | const float c = 0.18; // anchor for 18% grey
37 | const float c_d = 10.013; // output luminance of 18% grey (in nits)
38 | const float w_g = 0.14; // change in grey between different peak luminance
39 | const float t_1 = 0.04; // shadow toe or flare/glare compensation
40 | const float r_hit_min = 128.; // scene-referred value "hitting the roof" for SDR (e.g. when n = 100 nits)
41 | const float r_hit_max = 896.; // scene-referred value "hitting the roof" for when n = 10000 nits
42 |
43 | // Calculate output constants
44 | const float r_hit = r_hit_min + (r_hit_max - r_hit_min) * (log(n / n_r) / log(10000. / 100.));
45 | const float m_0 = (n / n_r);
46 | const float m_1 = 0.5 * (m_0 + sqrt(m_0 * (m_0 + 4. * t_1)));
47 | const float u = pow((r_hit / m_1) / ((r_hit / m_1) + 1), g);
48 | const float m = m_1 / u;
49 | const float w_i = log(n / 100.) / log(2.);
50 | const float c_t = c_d / n_r * (1. + w_i * w_g);
51 | const float g_ip = 0.5 * (c_t + sqrt(c_t * (c_t + 4. * t_1)));
52 | const float g_ipp2 = -(m_1 * pow((g_ip / m), (1. / g))) / (pow(g_ip / m, 1. / g) - 1.);
53 | const float w_2 = c / g_ipp2;
54 | const float s_2 = w_2 * m_1;
55 | const float u_2 = pow((r_hit / m_1) / ((r_hit / m_1) + w_2), g);
56 | const float m_2 = m_1 / u_2;
57 |
58 | TSParams p;
59 | p.n = n;
60 | p.n_r = n_r;
61 | p.g = g;
62 | p.t_1 = t_1;
63 | p.c_t = c_t;
64 | p.s_2 = s_2;
65 | p.u_2 = u_2;
66 | p.m_2 = m_2;
67 | p.forward_limit = 8.0 * r_hit;
68 | p.inverse_limit = n / (u_2 * n_r);
69 | p.log_peak = log10(n / n_r);
70 |
71 | return p;
72 | }
73 |
74 | /* --- Tone scale math --- */
75 | float tonescale_fwd(float x, // scene-referred input (i.e. linear ACES2065-1)
76 | TSParams params // struct of type TSParams
77 | )
78 | {
79 | // forward MM tone scale
80 | float f = params.m_2 * pow(max(0.0, x) / (x + params.s_2), params.g);
81 | float h = max(0., f * f / (f + params.t_1)); // forward flare
82 |
83 | return h * params.n_r; // output is luminance in cd/m^2
84 | }
85 |
86 | float tonescale_inv(float Y, // luminance in cd/m^2
87 | TSParams params // struct of type TSParams
88 | )
89 | {
90 | float Z = max(0., min(params.n / (params.u_2 * params.n_r), Y));
91 | float h = (Z + sqrt(Z * (4. * params.t_1 + Z))) / 2.;
92 | float f = params.s_2 / (pow((params.m_2 / h), (1. / params.g)) - 1.);
93 |
94 | return f; // output is scene-referred input
95 | }
--------------------------------------------------------------------------------
/lib/Lib.Academy.Utilities.ctl:
--------------------------------------------------------------------------------
1 |
2 | // urn:ampas:aces:transformId:v2.0:Lib.Academy.Utilities.a2.v1
3 | // Utilities
4 |
5 | //
6 | // Generic functions that may be useful for writing CTL programs
7 | //
8 |
9 | float min(float a,
10 | float b)
11 | {
12 | if (a < b)
13 | return a;
14 | else
15 | return b;
16 | }
17 |
18 | float max(float a,
19 | float b)
20 | {
21 | if (a > b)
22 | return a;
23 | else
24 | return b;
25 | }
26 |
27 | float min_f3(float a[3])
28 | {
29 | return min(a[0], min(a[1], a[2]));
30 | }
31 |
32 | float max_f3(float a[3])
33 | {
34 | return max(a[0], max(a[1], a[2]));
35 | }
36 |
37 | float clip(float v)
38 | {
39 | return min(v, 1.0);
40 | }
41 |
42 | float[3] clip_f3(float in[3])
43 | {
44 | float out[3];
45 | out[0] = clip(in[0]);
46 | out[1] = clip(in[1]);
47 | out[2] = clip(in[2]);
48 |
49 | return out;
50 | }
51 |
52 | float clamp(float in,
53 | float clampMin,
54 | float clampMax)
55 | {
56 | // Note: Numeric constants can be used in place of a min or max value (i.e.
57 | // use HALF_NEG_INF in place of clampMin or HALF_POS_INF in place of clampMax)
58 |
59 | return max(clampMin, min(in, clampMax));
60 | }
61 |
62 | float[3] clamp_f3(float in[3],
63 | float clampMin,
64 | float clampMax)
65 | {
66 | // Note: Numeric constants can be used in place of a min or max value (i.e.
67 | // use HALF_NEG_INF in place of clampMin or HALF_POS_INF in place of clampMax)
68 |
69 | float out[3];
70 | out[0] = clamp(in[0], clampMin, clampMax);
71 | out[1] = clamp(in[1], clampMin, clampMax);
72 | out[2] = clamp(in[2], clampMin, clampMax);
73 |
74 | return out;
75 | }
76 |
77 | float[3] f3_from_f(float a)
78 | {
79 | float f3[3] = {a, a, a};
80 | return f3;
81 | }
82 |
83 | float[3] add_f_f3(float a,
84 | float b[3])
85 | {
86 | float out[3];
87 | out[0] = a + b[0];
88 | out[1] = a + b[1];
89 | out[2] = a + b[2];
90 | return out;
91 | }
92 |
93 | float[3] pow_f3(float a[3],
94 | float b)
95 | {
96 | float out[3];
97 | out[0] = pow(a[0], b);
98 | out[1] = pow(a[1], b);
99 | out[2] = pow(a[2], b);
100 | return out;
101 | }
102 |
103 | float[3] pow10_f3(float a[3])
104 | {
105 | float out[3];
106 | out[0] = pow10(a[0]);
107 | out[1] = pow10(a[1]);
108 | out[2] = pow10(a[2]);
109 | return out;
110 | }
111 |
112 | float[3] log10_f3(float a[3])
113 | {
114 | float out[3];
115 | out[0] = log10(a[0]);
116 | out[1] = log10(a[1]);
117 | out[2] = log10(a[2]);
118 | return out;
119 | }
120 |
121 | float round(float x)
122 | {
123 | int x1;
124 |
125 | if (x < 0.0)
126 | x1 = x - 0.5;
127 | else
128 | x1 = x + 0.5;
129 |
130 | return x1;
131 | }
132 |
133 | float log2(float x)
134 | {
135 | return log(x) / log(2.);
136 | }
137 |
138 | int sign(float x)
139 | {
140 | // Signum function:
141 | // sign(X) returns 1 if the element is greater than zero, 0 if it equals zero
142 | // and -1 if it is less than zero
143 |
144 | int y;
145 | if (x < 0)
146 | {
147 | y = -1;
148 | }
149 | else if (x > 0)
150 | {
151 | y = 1;
152 | }
153 | else
154 | {
155 | y = 0;
156 | }
157 |
158 | return y;
159 | }
160 |
161 | float[3] sign_f3(float in[3])
162 | {
163 | float return_val[3] = {sign(in[0]),
164 | sign(in[1]),
165 | sign(in[2])};
166 | return return_val;
167 | }
168 |
169 | // returns a value combining the magnitude of x with the sign of y
170 | float copysign(float x,
171 | float y)
172 | {
173 | return sign(y) * fabs(x);
174 | }
175 |
176 | float ceil(float a)
177 | {
178 | return floor(a + 1.0);
179 | }
180 |
181 | float[3] vector_dot(float m[3][3], float v[3])
182 | {
183 | float r[3];
184 | r[0] = m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2];
185 | r[1] = m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2];
186 | r[2] = m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2];
187 | return r;
188 | }
189 |
190 | float[3][3] scale_matrix_diagonal_f33_f3(float A[3][3],
191 | float v[3])
192 | {
193 | float B[3][3] = A;
194 | B[0][0] = A[0][0] * v[0];
195 | B[1][1] = A[1][1] * v[1];
196 | B[2][2] = A[2][2] * v[2];
197 | return B;
198 | }
199 |
200 | // linear interpolation between two values a & b with the bias t
201 | float lerp(float a, float b, float t)
202 | {
203 | return a + t * (b - a);
204 | }
205 |
206 | float[3] lerp_f3(float a[3], float b[3], float t)
207 | {
208 | float lerped[3] = {lerp(a[0], b[0], t),
209 | lerp(a[1], b[1], t),
210 | lerp(a[2], b[2], t)};
211 | return lerped;
212 | }
213 |
214 | float radians_to_degrees(float radians)
215 | {
216 | return radians * 180.0 / M_PI;
217 | }
218 |
219 | float degrees_to_radians(float degrees)
220 | {
221 | return degrees / 180.0 * M_PI;
222 | }
223 |
224 | // safe power function to avoid NaNs or Infs when taking a fractional power of a negative base
225 | // this initially returned -pow(abs(b), e) for negative b but this ended up producing
226 | // undesirable results in some cases, so now it just returns 0.0 instead
227 | float spow(float base,
228 | float exponent)
229 | {
230 | if (base < 0.0 && exponent != floor(exponent))
231 | {
232 | return 0.0;
233 | }
234 | else
235 | {
236 | return pow(base, exponent);
237 | }
238 | }
239 |
240 | float[3] spow_f3(float base[3],
241 | float exponent)
242 | {
243 | float return_val[3] = {spow(base[0], exponent),
244 | spow(base[1], exponent),
245 | spow(base[2], exponent)};
246 | return return_val;
247 | }
248 |
249 | float[3] fabs_f3(float in[3])
250 | {
251 | float return_val[3] = {fabs(in[0]),
252 | fabs(in[1]),
253 | fabs(in[2])};
254 | return return_val;
255 | }
256 |
257 | // safe divide function - return 0 if a divide by zero
258 | float sdiv(float a,
259 | float b)
260 | {
261 | if (b == 0.0)
262 | {
263 | return 0.0;
264 | }
265 | else
266 | {
267 | return a / b;
268 | }
269 | }
270 |
271 | float[3][3] RGBtoXYZ_f33(Chromaticities C,
272 | float Y)
273 | {
274 | // X and Z values of RGB value (1, 1, 1), or "white"
275 | float X = C.white[0] * Y / C.white[1];
276 | float Z = (1. - C.white[0] - C.white[1]) * Y / C.white[1];
277 |
278 | // Scale factors for matrix rows
279 | float d = C.red[0] * (C.blue[1] - C.green[1]) +
280 | C.blue[0] * (C.green[1] - C.red[1]) +
281 | C.green[0] * (C.red[1] - C.blue[1]);
282 |
283 | float Sr = (X * (C.blue[1] - C.green[1]) -
284 | C.green[0] * (Y * (C.blue[1] - 1) + C.blue[1] * (X + Z)) +
285 | C.blue[0] * (Y * (C.green[1] - 1) + C.green[1] * (X + Z))) /
286 | d;
287 |
288 | float Sg = (X * (C.red[1] - C.blue[1]) +
289 | C.red[0] * (Y * (C.blue[1] - 1) + C.blue[1] * (X + Z)) -
290 | C.blue[0] * (Y * (C.red[1] - 1) + C.red[1] * (X + Z))) /
291 | d;
292 |
293 | float Sb = (X * (C.green[1] - C.red[1]) -
294 | C.red[0] * (Y * (C.green[1] - 1) + C.green[1] * (X + Z)) +
295 | C.green[0] * (Y * (C.red[1] - 1) + C.red[1] * (X + Z))) /
296 | d;
297 |
298 | // Assemble the matrix
299 | float M[3][3];
300 |
301 | M[0][0] = Sr * C.red[0];
302 | M[0][1] = Sr * C.red[1];
303 | M[0][2] = Sr * (1. - C.red[0] - C.red[1]);
304 |
305 | M[1][0] = Sg * C.green[0];
306 | M[1][1] = Sg * C.green[1];
307 | M[1][2] = Sg * (1. - C.green[0] - C.green[1]);
308 |
309 | M[2][0] = Sb * C.blue[0];
310 | M[2][1] = Sb * C.blue[1];
311 | M[2][2] = Sb * (1. - C.blue[0] - C.blue[1]);
312 |
313 | return M;
314 | }
315 |
316 | float[3][3] XYZtoRGB_f33(Chromaticities C,
317 | float Y)
318 | {
319 | return invert_f33(RGBtoXYZ_f33(C, Y));
320 | }
321 |
322 | float[3] HSV_to_RGB(float HSV[3])
323 | {
324 | float C = HSV[2] * HSV[1];
325 | float X = C * (1. - fabs(fmod(HSV[0] * 6., 2.) - 1.));
326 | float m = HSV[2] - C;
327 |
328 | float RGB[3];
329 | if (HSV[0] < 1. / 6.)
330 | {
331 | RGB[0] = C;
332 | RGB[1] = X;
333 | RGB[2] = 0.;
334 | }
335 | else if (HSV[0] < 2. / 6.)
336 | {
337 | RGB[0] = X;
338 | RGB[1] = C;
339 | RGB[2] = 0.;
340 | }
341 | else if (HSV[0] < 3. / 6.)
342 | {
343 | RGB[0] = 0.;
344 | RGB[1] = C;
345 | RGB[2] = X;
346 | }
347 | else if (HSV[0] < 4. / 6.)
348 | {
349 | RGB[0] = 0.;
350 | RGB[1] = X;
351 | RGB[2] = C;
352 | }
353 | else if (HSV[0] < 5. / 6.)
354 | {
355 | RGB[0] = X;
356 | RGB[1] = 0.;
357 | RGB[2] = C;
358 | }
359 | else
360 | {
361 | RGB[0] = C;
362 | RGB[1] = 0.;
363 | RGB[2] = X;
364 | }
365 | RGB = add_f_f3(m, RGB);
366 | return RGB;
367 | }
368 |
369 | // smooth minimum of a and b
370 | float smin(float a, float b, float s)
371 | {
372 | float h = max(s - fabs(a - b), 0.0) / s;
373 | return min(a, b) - h * h * h * s * (1.0 / 6.0);
374 | }
375 |
376 | bool f2_equal_to_tolerance(float a[2],
377 | float b[2],
378 | float tolerance)
379 | {
380 | return (fabs(a[0] - b[0]) <= tolerance && fabs(a[1] - b[1]) <= tolerance);
381 | }
382 |
383 | // Print functions for various data types
384 | void print_f2(float m[2])
385 | {
386 | print(m[0], ",\t", m[1], "\n");
387 | }
388 |
389 | void print_f3(float m[3])
390 | {
391 | print(m[0], ",\t", m[1], ",\t", m[2], "\n");
392 | }
393 |
394 | void print_f33(float m[3][3])
395 | {
396 | print("{ {", m[0][0], ",\t", m[0][1], ",\t", m[0][2], "},\n");
397 | print(" {", m[1][0], ",\t", m[1][1], ",\t", m[1][2], "},\n");
398 | print(" {", m[2][0], ",\t", m[2][1], ",\t", m[2][2], "} };\n");
399 | }
400 |
401 | void print_f44(float m[4][4])
402 | {
403 | print("{ { ", m[0][0], ",\t", m[0][1], ",\t", m[0][2], ",\t", m[0][3], "},\n");
404 | print(" { ", m[1][0], ",\t", m[1][1], ",\t", m[1][2], ",\t", m[1][3], "},\n");
405 | print(" { ", m[2][0], ",\t", m[2][1], ",\t", m[2][2], ",\t", m[2][3], "},\n");
406 | print(" { ", m[3][0], ",\t", m[3][1], ",\t", m[3][2], ",\t", m[3][3], "} };\n");
407 | }
408 |
409 | void print_table_f(float t[])
410 | {
411 | print("\n");
412 | for (int i = 0; i < t.size; i = i + 1)
413 | {
414 | print(t[i], "\n");
415 | }
416 | }
417 |
418 | void print_table_i(int t[])
419 | {
420 | print("\n");
421 | for (int i = 0; i < t.size; i = i + 1)
422 | {
423 | print(t[i], "\n");
424 | }
425 | }
426 |
427 | void print_table_f3(float t[][3])
428 | {
429 | print("\n");
430 | for (int i = 0; i < t.size; i = i + 1)
431 | {
432 | print(t[i][0], "\t", t[i][1], "\t", t[i][2], "\n");
433 | }
434 | }
--------------------------------------------------------------------------------