function varargout = PsychColorCorrection(cmd, varargin) % varargout = PsychColorCorrection(cmd, varargin) % % This function is used to setup and control the built-in color correction % and transformation mechanisms of PTB's imaging pipeline. It allows to choose % among different methods of color correction (for calibrated output of % stimuli) and to change parameters of the chosen method. This only works % when the imaging pipeline is enabled and with certain output devices. % % This functions are mostly meant to provide fast color correction, e.g., % gamma correction, when PTB is used with special high precision / HDR % output devices, e.g., CRS Bits++ in Mono++ or Color++ mode, or video % attenuators. When operating such devices, the standard gamma correction % mechanisms of your graphics card can't be used, so PTB must do it itself. % % With a standard 8bpc framebuffer, you won't need this function for simple % gamma correction or color correction. % Instead you'd use the Screen('LoadNormalizedGammaTable') command to % perform gamma correction with the graphics cards built-in gamma tables. % Same applies to Bits++ box in Bits++ mode, using the built-in tables of % the Bits++ device, also controlled via Screen('LoadNormalizedGammaTable'). % % For more complex correction schemes than a lookup table transform you can % still use this function, even with a normal 8 bit framebuffer, as long as % the imaging pipeline is enabled. It's just important to note that you % don't need to use it for a standard framebuffer in the simple case -- the % gfx cards gamma tables are more efficient for the simple case. % % How to use: % % 1. Before opening an onscreen window, you use the following % PsychImaging() setup call to specify the method of display color correction % to apply, and the view channel to apply it to (in case there are multiple % like in many stereo display setups): % % PsychImaging('AddTask', whichChannel, 'DisplayColorCorrection', methodname); % - where whichChannel can be 'LeftView' or 'RightView' for the left- or % right display output channel/device of a stereo setup, or % 'FinalFormatting' if you have a single display monoscopic setup or want % to apply the same color correction to both channels of a stereo setup. % The parameter 'methodname' is a name string with the name of one of the % supported methods - See overview below for supported methods. % % CAUTION: The order of specifications matters! If you use multiple color % correction methods or other image processing operations simultaneously, % make sure to specify them in the order in which they should be executed. % The system tries to order operations in a reasonable way, but it is not % fool-proof! % % 2. Then, after you've specified all other window parameters via the % PsychImaging() subcommands, you open the onscreen window via the usual % win = PsychImaging('OpenWindow', ....); call, as always when you use the % imaging pipeline. This will open the window and apply the chosen color % correction method, choosing reasonable default parameters for the method % at hand. % % 3. At any time in your script you can change the operating parameters of % the chosen color correction method via the PsychColorCorrection() % subfunctions mentioned below. You'll have to specify the window handle % for the onscreen window whose parameters should be changed, and - in a % stereo display setup with separate color correction parameters for each % of the display channels - the name 'whichChannel' of the display channel % to change, e.g., 'LeftView'. Changes take effect at next Screen('Flip'). % % % % Supported display color correction methods: % =========================================== % % These are the method names that can be passed as 'methodname' parameter to % PsychImaging('DisplayColorCorrection', ....., methodname): % % 'methodname' is the name string of one of the supported methods: % % * 'None': Don't do anything. Fastest, but not safest. % % If your stimulus contains (by accident) color or luminance values outside % the displayable range, the corresponding pixels may show undefined output % color -- Likely not what you want. This mode is about 10% faster than % 'ClampOnly'. This mode is the default if no method is selected, unless % you use Bits++ in Mono++ or Color++ mode, where 'ClampOnly' is the % default. % % % * 'ClampOnly': Do not apply any color transformation. Default for Bits++ % in Mono++ or Color++ mode: % % In this mode, values are clamped against the limits set via % 'SetColorClampingRange' (see below) to always keep them in the % requested range set via PsychColorCorrection('SetColorClampingRange'), % nothing else. % % % * 'CheckOnly': Do not apply any color transformation. % % In this mode, values are checked against the limits set via % PsychColorCorrection('SetColorClampingRange'). Values outside that % range are visually coded, so you should be able to see troublesome % areas via visual inspection: All values are first clamped, then % inverted, in the hope that this creates clearly detectable artifacts in % your stimulus. We may change the "marker" method in the future if we % happen to find a better visual marker. This mode is meant for debugging % your stimulation scripts, not for running them on your subjects! % % % * 'SimpleGamma' : Apply some power-law based gamma correction: % % Simple gamma correction means: Apply a power-law to incoming % values: outcolor = incolor ^ EncodingGamma. incolor is the uncorrected % input color or intensity, outcolor the corrected one, EncodingGamma is % encoding gamma value to apply. See PsychColorCorrection('SetEncodingGamma') % for how to set the gamma values. After gamma correction, output values % are clamped against the range set via PsychColorCorrection('SetColorClampingRange'). % % See PsychColorCorrection('SetExtendedGammaParameters') on how to supply % additional parameters for use of a more complex gamma correction function. % % % * 'MatrixMultiply4' : Multiply with 4x4 color transformation matrix: % % Extend RGB component of color by a fourth "constant 1" component, % multiply by specified 4-by-4 matrix, normalize by dividing result by % 4'th component, return transformed rgb components as output color. Pass % through alpha-channel unmodified. % % rgbw' = M * [r,g,b,1]' % out.rgb = [r', g', b'] / w' % out.a = a % % * 'SensorToPrimary' : Implement the same conversion as SensorToPrimary.m % % This can convert from XYZ sensor tristimulus values to standard RGB % primary color values. This is the right thing to do if your display % device is linearized via use of a proper gamma correction method. % % Use the function PsychColorCorrection('SetSensorToPrimary') below to % assign the 'cal'ibration struct to use for actual conversion from XYZ % color values to RGB primaries. % % % * 'xyYToXYZ' : Implement the same conversion as xyYToXYZ.m % % This can convert to tristimulus XYZ color coordinates from % chromaticity (x,y) and luminance (Y) color coordinates xyY. It performs % the same color space conversion as the M-Function XYZ = xyYToXYZ(xyY). % % % * 'LookupTable' : Apply color correction by color table lookup, ie. a CLUT. % % This will allow to pass in a color lookup table of selectable % granularity (ie., number of slots) and range, which is later on used to % lookup corresponding corrected color values for given framebuffer input % values. % % % * 'LookupTable3D' : Apply color correction by a 3D color table lookup. % % This will allow to pass in a 3D color lookup table of selectable % granularity (ie., number of slots per dimension) and range, which is % later on used to lookup corresponding corrected color values for given % framebuffer input values. % % % * 'GainMatrix' : Apply color gain correction by 2D gain matrix lookup. % % This allows to apply a 2D matrix G which stores luminance- or color gain % correction factors for each single output pixel of the display. For % each 2D display pixel location (x,y), the stimulus image I(x,y) will be % multiplied with the corresponding gain factor G(x,y) of the gain matrix % and the result O(x,y) used for further processing and display. G(x,y) % can be a single scalar for luminance correction, or - if G is a 3-layer % matrix - a RGB vector with individual gains for each color channel and % pixel location. % % O(x,y) = I(x,y) * G(x,y). % % If you want to combine this with one of the other correction methods, % e.g., gamma correction, you should issue this command first, because % the non-linear gamma correction should apply to the output of this % method for correct results. % % After you've opened your onscreen 'window', you'll need to define the 2D % gain 'matrix' via a call to ... % % PsychColorCorrection('SetGainMatrix', ...); % % See below for description of 'SetGainMatrix'. % % * 'AnaglyphStereo' : Apply anaglyph stereo algorithm. % % This loads a similar anaglyph shader as the one used in stereoModes 6 % to 9 when the function SetAnaglyphStereoParameters('ColorAnaglyphMode') % or its siblings was used. This is useful if you want to employ anaglyph % stereo presentation, but not by rendering a full anaglyph image into % one single framebuffer for output to a single display or projector, but % if you want to direct the "left-eye" anaglyph image to a different % display or projector than the "right-eye" anaglyph image. An example % would be having two video projectors attached to two video outputs of a % graphics card. One projector shall project the left-eye image, the % other one the right-eye image. In this case you'd choose a stereoMode % of 4 or 5 (typically on Linux or Windows) for desktop spanning stereo % output, or 10 (on OSX) for dual-window stereo output. Then you'd use ... % PsychImaging('AddTask', 'LeftView', 'DisplayColorCorrection', 'AnaglyphStereo') % ... and ... % PsychImaging('AddTask', 'RightView', 'DisplayColorCorrection', 'AnaglyphStereo') % ... to add an anaglyph shader to the end of each view channel. % % After adding individual shaders for each image processing channel and opening % the onscreen window(s), you can use SetAnaglyphStereoParameters() to % parameterize the anaglyph stereo presentation as if it were created via % regular anaglyph stereo setup in stereomoded 6-9. % % % Supported runtime Subfunctions: % =============================== % % oldlevel = PsychColorCorrection('Verbosity' [, newlevel]); % Return current level of verbosity in optional 'oldlevel', optionally set % new level of verbosity via 'newlevel'. The level of verbosity affects how % much status output is printed in some routines: 0 = Nothing, 1 = Only % errors, 2 = Errors and warnings, 3 = Errors + Warnings + Some info. % % % The following routines must be called *after* opening a window for which color % correction is enabled. They can be called anytime and changed settings % will apply at the next Screen('Flip'). If your 'window' is actually a % stereo display window, you may want or need to provide the optional % 'viewId' parameter to tell PTB which of the two stereo view channels % shall be changed in its settings. If both stereo views are displayed on % the same physical display device, this is not needed. If the separate % views go to separate physical displays, you may need to calibrate them % separately. Allowable values for viewId are 'AllViews', 'LeftView' and % 'RightView', corresponding to the 'whichChannel' setting that you used % when setting up the window with PsychImaging(). % % % PsychColorCorrection('SetColorClampingRange', window, min, max [,viewId]); % - Set the range of allowable output color or luminance intensity values % to the interval [min; max] for onscreen window 'window'. Values outside % that range get either clamped to the 'min'imum or 'max'imum value, or - % in 'CheckOnly' mode - will be visually marked as out of range. The default % range is [0.0 ; 1.0] -- The range that your display device can really % display. % % % PsychColorCorrection('SetEncodingGamma', window, gamma [,viewId]); % - Set the gamma value to use for gamma correction on window 'window'. % 'gamma' can be either a single scalar if the same gamma should apply to % all color channels (or single luminance channel), or it can be a % three-component [gammaRed, gammaGreen, gammaBlue] vector, if each color % channel should be gamma corrected with an individual gamma value. % % How the value gets applied depends on the chosen method of color % correction (see above). The simplest method ('SimpleGamma') performs a % simple power-law mapping of input values to output values: out = in ^ gamma. % 'in' must be greater than zero, and 'gamma' must be greater than zero, % otherwise results may be undefined, depending on your graphics hardware. % However, usually only encoding 'gamma' values in the range of about % 0.33 - 1.0 are meaningful. % % Example: If your monitor has a "decoding gamma" of 1.8, the proper % setting for 'gamma' would be gamma = 1/1.8. For a decoding gamma of 2.2, % you'd choose gamma = 1/2.2 ... % % % PsychColorCorrection('SetExtendedGammaParameters', window, minL, maxL, gain, bias [,viewId]); % - Set the additional (optional) parameters to fine-tune gamma correction on % window 'window'. All these parameters have reasonable defaults. All % parameters can be supplied as a scalar value if the same setting shall % apply to all color channels (or a single luminance channel), or you can % provide 3-component vectors with one component for each color channel: % % After this function has been called at least once, the following formula % will be used to map input values to output values ['gamma' is as set by % the 'SetEncodingGamma' function, 'in' is input, 'out' is output value]: % % out = bias + gain * ( ((in-minL) / (maxL-minL)) ^ gamma ) % % Required parameters: % 'minL' Minimum expected input luminance/intensity value (Default is 0.0). % 'maxL' Maximum expected input luminance/intensity value (Default is 1.0). % 'gain' Gain factor to apply after power-law mapping (Default is 1.0). % 'bias' Bias/Offset to apply to final result before output (Default is 0.0). % % % PsychColorCorrection('SetMultMatrix4', window, matrix [, viewId]); % - Set the 4-by-4 color transformation matrix to use for the 'MatrixMultiply4' % color correction method. 'matrix' must be the 2D 4 rows by 4 columns % matrix to use. By default, the matrix is set to an identity matrix. % % % PsychColorCorrection('SetSensorToPrimary', window, cal [, viewId]); % - Set the 'cal'ibration struct to use for the 'SensorToPrimary' color % correction method. 'cal' must be the same input format as used for the % M-Function SensorToPrimary(). By default, the transformation is a "no % operation". % % % PsychColorCorrection('SetLookupTable', window, clut [, viewId][, maxinput=1][, scalefactor]); % - Assign color lookup table 'clut' for use with color correction method % 'LookupTable'. 'clut' must be a 1 column vector for pure luminance lookup % tables, or a 3 column matrix for RGB color lookup tables with one column % per color channel, ie., [1,2,3] = [Red, Green, Blue]. clut must have at % least 1 row, but usually will have way more than 2 rows, typically almost % as many rows as n = 2^bpc for a given output device bitdepths bpc. For a % 10 bit output device, n would be usually 2^10 = 1024 rows for a perfect % one-to-one mapping. At runtime, color correction will be performed by the % following formula: Be Rin, Gin, Bin the input red, green and blue color % components, and Rout, Gout, Bout the final output value for the % framebuffer. First Rin, Gin and Bin are clamped individually to the range % 0.0 - 'maxinput' (maxinput is 1.0 by default), scalefactor is chosen by % default as scalefactor = [number of rows in clut - 1] / maxinput, ie., it % maps the possible input range 0 - maxinput to the full range of row % indices 1 - rowcount to cover the full range of entries stored in the % clut. This is the most reasonable default, but can be changed by the % optional 'scalefactor' and 'maxinput' arguments. % % Then the output color for each component is looked up in the proper slot % (= row index) of the passed clut: % % Rout = clut(Rin * scalefactor,1); % Gout = clut(Gin * scalefactor,2); % Bout = clut(Bin * scalefactor,3); % % Color values for fractional indices inbetween reference values in the % clut are interpolated linearly between the two nearest neighbour % reference values --> linear interpolation. % % Finally, Rout, Gout and Bout are clamped to the valid output range as set % by the function PsychColorCorrection('SetColorClampingRange', ...); by % default to the range 0.0 - 1.0. % % % PsychColorCorrection('SetLookupTable3D', window, clut [, viewId][, maxinput=1][, scalefactor][, precision=0][, interpolate=1]); % - Assign 4D color lookup table 'clut' for use with color correction method % 'LookupTable3D'. 'clut' must be a 4D 3-by-m-by-n-by-p matrix. The first dimension encodes the % output color values to use: % clut(1,r,g,b) == Corrected red color value for input color [r,g,b]. % clut(2,r,g,b) == Corrected green color value for input color [r,g,b]. % clut(3,r,g,b) == Corrected blue color value for input color [r,g,b]. % % clut must have at least one element in each color index dimension, ie., m, n % and p must be >= 1, but usually will have more elements in each dimension % for a meaningful lookup color correction. In theory you would need m, n % and p to be == 2^bpc for a given output device bitdepths bpc, e.g, for a % 8 bit output device, m,n,p would need to be 2^8 = 256 elements for a perfect % one-to-one mapping. In reality you likely don't want to use such large % sizes, as such a huge and dense 3D CLUT would take up considerable % amounts of graphics memory and cause large slowdowns of all drawing % operations. For good performance and portability of your code to older % graphics cards, choose modest sizes, as small as possible. % % At runtime, color correction will be performed by the following 3D table % lookup procedure: Let Rin, Gin, Bin be the input red, green and blue % color components, and Rout, Gout, Bout the final output values for the % framebuffer. First Rin, Gin and Bin are clamped individually to the range % 0.0 - 'maxinput' (maxinput is 1.0 by default), scalefactor is chosen by % default as scalefactor = (1.0 / maxinput), ie., it maps the possible % input range 0 - 'maxinput' to the range 0.0 - 1.0, which covers the full % range of entries stored in the clut. This is the most reasonable default, % but can be changed by the optional 'scalefactor' and 'maxinput' % arguments. % % Then the output color for each component is looked up in the proper 3D % slot of the passed clut: % % Rout = clut(1, Rin * scalefactor, Gin * scalefactor, Bin * scalefactor); % Gout = clut(2, Rin * scalefactor, Gin * scalefactor, Bin * scalefactor); % Bout = clut(3, Rin * scalefactor, Gin * scalefactor, Bin * scalefactor); % % Color values (Rin, Gin, Bin) == (0,0,0) map to the first elements in the % cluts dimensions, ie., clut(:,1,1,1). Maximum values (maxinput, maxinput, % maxinput) map - after scaling with the default scalefactor - to % coordinates (1.0, 1.0, 1.0) in the normalized 3D color coordinate space % and are looked up in the maximal clut element indices of each dimension, % ie., clut(:,m,n,p) for our 3-by-m-by-n-by-p clut. Intermediate values are % mapped accordingly to proper clut element indices. % % By default, color values for fractional indices inbetween reference % values in the clut are interpolated linearly between the eight nearest % neighbour reference values in the 3 dimensional space --> This is % trilinear interpolation across all 3 color dimensions of the CLUT. If you % set the optional parameter 'interpolate' to zero, then simple nearest % neighbour sampling is performed instead. % % The optional 'precision' flag controls the precision with which entries % in the CLUT should be stored and processed: precision = 0 is the default % and stores values with 8 bit precision for 256 different intensity levels % for Rout, Gout and Bout. A setting of 1 will store values with 16 bpc % floating point precision to resolve up to 10 bits or 1024 levels of % linear precision. A setting of 2 will store values with 32 bpc floating % point precision for up to 23 bits of linear precision. Be aware that % precision values > 0 will increase memory consumption by a factor of 2x % or 4x, which can be significant for lookup tables of non-trivial size. % % Final looked up, Rout, Gout and Bout are clamped to the valid output range as set % by the function PsychColorCorrection('SetColorClampingRange', ...); by % default to the range 0.0 - 1.0. % % Essentially, this color correction allows to define arbitrary mappings of % RGB input triplets to RGB output triplets, providing a large amount of % flexibility. Be aware though that strongly discontinuous mappings of input % colors to output colors can have a significant negative impact on the % drawing performance of your graphics card. Define your clut's wisely! % % % PsychColorCorrection('SetGainMatrix', window, matrix [, viewId][, precision=2]); % % - Set gain matrix for method 'GainMatrix'. % If matrix is a 2D matrix, the gain will be applied to all color % channels equally. If matrix is a 3D matrix, matrix(y,x,1) will define % the red channel gain, matrix(y,x,2) will define the green channel gain, % and matrix(y,x,3) will define the blue channel gain. % % The optional 'precision' parameter defines the numerical precision with % which the gain factors are stored. The default setting of 2 stores with % 32 bit floating point precision - about 6 digits behind the decimal % point. A Setting of 1 stores with 16 bit float precision, about 3 digits. % A Setting of 0 stores with 256 levels, about 2 digits. A lower precision % is less precise but allows for faster processing and higher redraw rates % if needed. % % % % Internal commands, usually not meant for direct use by pure mortals: % ==================================================================== % % All these methods are usually called from within PsychImaging() to do the % dirty setup work... % % Call this *before* opening a window: % % % PsychColorCorrection('ChooseColorCorrection', methodname); % - Specify the method to be used for color correction for the next % onscreen window that will be opened. This needs to be called *before* the % window is opened, however its usually done automatically at the right % moment by routines like PsychImaging() or BitsPlusPlus() if you use these % to open windows. % % % Called after Screen('OpenWindow') during shader and pipeline setup: % % [shader, idstring, configString, overrideMain] = PsychColorCorrection('GetCompiledShaders', window, debuglevel); % - Compile corresponding shaders for chosen color correction method, % return shaderhandles and idstring to calling routine. That routine will % link the returned shaders with other shader code to produce the final % GLSL program object for color conversion and output formatting. 'shader' % is the GLSL shader handle, idstring the name string for the shader, % configString the shader option string for the 'Hookfunction' call, e.g., % to bind additional LUT textures, etc. % % % Called after linking and attaching the final processing GLSL program % objects and slots to the imaging pipelines hook chain(s): % % % PsychColorCorrection('ApplyPostGLSLLinkSetup', window, viewId); % - Perform whatever setup work is needed after final GLSL program object % has been created and attached to imaging pipeline. % % History: % 16.04.2008 Written (MK). % 18.05.2008 Revised, improved help text, fine-tuning etc. (MK) % 04.07.2009 Add CLUT based color correction. (MK) % 10.10.2009 Add 'SetExtendedGammaParameters' for extended gamma correction. (MK) % 05.03.2010 Add 'GainMatrix' and 'SetGainMatrix' for display % shading/vignetting correction. (MK) % 14.08.2012 Add 'AnaglyphStereo' to apply anaglyph stereo mode onto % separate display outputs, e.g., as on MPI Kuka projection % setup. (MK) % 21.08.2012 Add support for 3D CLUTs for "color cube" lookups. (MK) % 21.08.2012 Add 'Verbosity' level support. (MK) % 24.08.2012 Add 'SetMultMatrix4' support for 4-by-4 matrix multiply. (MK) % 25.01.2013 Add 'SensorToPrimary' XYZ -> RGB colorspace conversion. (MK) % 26.01.2013 Add 'xyYToXYZ' xyY -> XYZ colorspace conversion. (MK) % GL is needed for shader setup and parameter changes: global GL; persistent verbosity; persistent specReady; persistent icmSpec; persistent icmDataForHandle; if isempty(specReady) % We default to a setting of 'ClampOnly' if no spec specified. Usercode can % override this... specReady = 1; icmSpec.type = 'ClampOnly'; verbosity = 3; end % Child protection: if nargin < 1 error('Subcommand specification missing!'); end if isempty(cmd) error('Subcommand specification missing!'); end % Subcommand dispatch: if strcmpi(cmd, 'ChooseColorCorrection') if nargin < 2 error('You must specify a color-correction method!'); end % Assign method, that's it... icmSpec.type = varargin{1}; specReady = 1; return; end if strcmpi(cmd, 'Verbosity') varargout{1} = verbosity; if nargin == 2 verbosity = varargin{1}; end return; end % Retrieve a (previously specified) collection of precompiled shader % objects. These implement the subroutines for color processing, according % to given spec. if strcmpi(cmd, 'GetCompiledShaders') if ~specReady error('"GetCompiledShaders" called, but specification of what to compile is unavailable!'); end if nargin >= 3 debuglevel = varargin{2}; else debuglevel = 0; end % Need GL from here on... if isempty(GL) InitializeMatlabOpenGL([], [], 1); end % No config string by default: icmConfig = ''; % No override main routine by default: icmOverrideMain = ''; switch(icmSpec.type) case {'None'} % Load pass-through shader: icmShaders = LoadShaderFromFile('ICMPassThroughShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); case {'ClampedNoName'} % Load clamped pass-through shader: icmShaders = LoadShaderFromFile('ICMClampedPassThroughShader.frag.txt', [], debuglevel); % but set its id string to some special string that is hidden from standard search: icmIdString = 'CMIH:ClampedNoName'; case {'ClampOnly'} % Load clamped pass-through shader: icmShaders = LoadShaderFromFile('ICMClampedPassThroughShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); case {'CheckOnly'} % Load checked pass-through shader: icmShaders = LoadShaderFromFile('ICMCheckedPassThroughShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); % Simple power-law bog-standard gamma correction. case {'SimpleGamma'} % Load our bog-standard power-law shader: icmShaders = LoadShaderFromFile('ICMSimpleGammaCorrectionShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); case {'MatrixMultiply4', 'SensorToPrimary'} % Load our matrix multiply shader: icmShaders = LoadShaderFromFile('ICMMatrixMult4Shader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); case {'xyYToXYZ'} % Load our xyYToXYZ colorspace conversion shader: icmShaders = LoadShaderFromFile('ICMConvert_xyYToXYZ_Shader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); % Color correction by CLUT texture lookup table operation: case {'LookupTable'} % Load our 1D CLUT color correction shader: icmShaders = LoadShaderFromFile('ICMCLUTCorrectionShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); % Generate texture handle for fillout later on: icmSpec.icmlutid = glGenTextures(1); % Build config string to bind and use our CLUT texture: icmConfig = sprintf('TEXTURERECT2D(2)=%i', icmSpec.icmlutid); % Color correction by indexing into 3D texture lookup table: case {'LookupTable3D'} % Load our 3D CLUT color correction shader: icmShaders = LoadShaderFromFile('ICM3DCLUTCorrectionShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); % Generate texture handle for fillout later on: icmSpec.icmlutid = glGenTextures(1); % Build config string to bind and use our CLUT 3D texture: icmConfig = sprintf('TEXTURE3D(2)=%i', icmSpec.icmlutid); % Vignetting correction by lookup into a 2D per-pixel gain texture: case {'GainMatrix'} % Load our 2D gain correction shader: icmShaders = LoadShaderFromFile('ICM2DGainCorrectionShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); % Generate texture handle for fillout later on: icmSpec.icmlutid = glGenTextures(1); % Build config string to bind and use our gain texture: icmConfig = sprintf('TEXTURERECT2D(2)=%i', icmSpec.icmlutid); case {'AnaglyphStereo'} % Load the single-view anaglyph shader: icmShaders = LoadShaderFromFile('ColoredSingleChannelAnaglyphShader.frag.txt', [], debuglevel); icmIdString = sprintf('ICM:%s', icmSpec.type); otherwise error('Unknown type of color correction requested! Internal bug?!?'); end % Return Vector of shader handles: varargout{1} = icmShaders; % Return id string: varargout{2} = icmIdString; % Return shader config string: varargout{3} = icmConfig; % Return override main function definition string: varargout{4} = icmOverrideMain; return; end if strcmpi(cmd, 'ApplyPostGLSLLinkSetup') if ~specReady error('"ApplyPostGLSLLinkSetup" called, but specification of what to postlink is unavailable!'); end % Need GL from here on... if isempty(GL) error('No GL struct defined in "ApplyPostGLSLLinkSetup"?!? This is a bug - Check code!!'); end if nargin < 3 error('Must provide window handle to onscreen window as 2nd argument and viewId as 3rd one!'); end % Fetch window handle: win = varargin{1}; viewId = varargin{2}; % Retrieve all params for 'win'dow and given icmSpec, bind shader: [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmSpec.type, viewId); %#ok<*ASGLU,NASGU> try % Setup initial clamping values to valid range 0.0 - 1.0: glUniform2f(glGetUniformLocation(glsl, 'ICMClampToColorRange'), 0.0, 1.0); switch(icmSpec.type) case {'ClampOnly', 'CheckOnly', 'None', 'ClampedNoName'} % Nothing to do yet... case {'SimpleGamma'} % Set default encoding gamma for power-law shader to (1.0, 1.0, 1.0): glUniform3f(glGetUniformLocation(glsl, 'ICMEncodingGamma'), 1.0, 1.0, 1.0); % Default min and max luminance is 0.0 to 1.0, therefore reciprocal 1/range is also 1.0: glUniform3f(glGetUniformLocation(glsl, 'ICMMinInLuminance'), 0.0, 0.0, 0.0); glUniform3f(glGetUniformLocation(glsl, 'ICMMaxInLuminance'), 1.0, 1.0, 1.0); glUniform3f(glGetUniformLocation(glsl, 'ICMReciprocalLuminanceRange'), 1.0, 1.0, 1.0); % Default gain to postmultiply is 1.0: glUniform3f(glGetUniformLocation(glsl, 'ICMOutputGain'), 1.0, 1.0, 1.0); % Default bias to is 0.0: glUniform3f(glGetUniformLocation(glsl, 'ICMOutputBias'), 0.0, 0.0, 0.0); case {'LookupTable'} % Set CLUT texture unit to 2: glUniform1i(glGetUniformLocation(glsl, 'ICMCLUT'), 2); % Setup everything to a pretty meaningless but safe % mapping, which will likely just produce all-white, % regardless of input: glUniform1f(glGetUniformLocation(glsl, 'ICMPrescale'), 1.0); glUniform1f(glGetUniformLocation(glsl, 'ICMMaxInputValue'), 1.0); % Note that we won't setup the CLUT texture yet. This is a % mandatory step after initial setup of the display. We do % store the texture id of the clut texture in a permanent % location though: icmDataForHandle(win, glsl) = icmSpec.icmlutid; case {'LookupTable3D'} % Set CLUT texture unit to 2: glUniform1i(glGetUniformLocation(glsl, 'ICMCLUT'), 2); % Setup everything to a pretty meaningless but safe % mapping, which will likely just produce all-white, % regardless of input: glUniform1f(glGetUniformLocation(glsl, 'ICMPrescale'), 1.0); glUniform1f(glGetUniformLocation(glsl, 'ICMMaxInputValue'), 1.0); % Note that we won't setup the CLUT texture yet. This is a % mandatory step after initial setup of the display. We do % store the texture id of the clut texture in a permanent % location though: icmDataForHandle(win, glsl) = icmSpec.icmlutid; case {'GainMatrix'} % Set Gain matrix texture unit to 2: glUniform1i(glGetUniformLocation(glsl, 'ICMGainField'), 2); % Note that we won't setup the Gain texture yet. This is a % mandatory step after initial setup of the display. We do % store the texture id of the texture in a permanent % location though: icmDataForHandle(win, glsl) = icmSpec.icmlutid; case {'AnaglyphStereo'} % Set RedGamma to zero, ie., "disabled" by default: glUniform1f(glGetUniformLocation(glsl, 'RedGamma'), 0.0); % Set 3x3 transform matrix to identity matrix by default: glUniformMatrix3fv(glGetUniformLocation(glsl, 'GainsLeft'), 1, 0, [[1 0 0]; [0 1 0]; [0 0 1]]); case {'MatrixMultiply4', 'SensorToPrimary'} % Set 4x4 transform matrix to identity matrix by default: glUniformMatrix4fv(glGetUniformLocation(glsl, 'M'), 1, 0, [[1 0 0 0]; [0 1 0 0]; [0 0 1 0]; [0 0 0 1]]); case {'xyYToXYZ'} % Nothing to do for xyYToXYZ colorspace conversion shader. otherwise error('Unknown type of color correction requested! Internal bug?!?'); end catch %#ok<*CTCH> % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); return; end if strcmpi(cmd, 'SetColorClampingRange') % Need GL from here on... if isempty(GL) error('No internal GL struct defined in "SetColorClampingRange" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 4 error('Must provide minimum and maximum allowable value in 3rd and 4th argument!'); end % Fetch window handle: win = varargin{1}; % Fetch values: minc = varargin{2}; maxc = varargin{3}; if ~isnumeric(minc) || ~isnumeric(maxc) error('(min, max) values must be numbers!'); end if minc > maxc error('Provided minimum value greater than maximum value - This will not work!'); end if nargin < 5 viewId = []; else viewId = varargin{4}; end % Retrieve all params for 'win'dow and given icmSpec, bind shader. The % 'icmSpec' string is empty - This will expand into the general 'ICM:' % string, so we use the first slot/shader with the ICM: token. icmId = ''; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Setup clamping values to given range: glUniform2f(glGetUniformLocation(glsl, 'ICMClampToColorRange'), minc, maxc); catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); return; end if strcmpi(cmd, 'SetEncodingGamma') % Need GL from here on... if isempty(GL) error('No internal GL struct defined in "SetEncodingGamma" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('Must provide encoding gamma value or vector of 3 encoding gammas for R,G,B in 3rd argument!'); end % Fetch window handle: win = varargin{1}; % Fetch values: gammas = varargin{2}; if ~isnumeric(gammas) error('Power law encoding gamma value(s) must be number(s)!'); end if length(gammas)~=1 && length(gammas)~=3 error('Encoding gamma must be a single scalar or a 3 component vector of separate gammas for the Red, Green and Blue color channel!'); end if any(min(gammas) < 0) || any(max(gammas) > 1) warning(sprintf('At least one of the encoding gamma values %f is outside the sensible range 0.0 - 1.0.\nThis will result in undefined behaviour and is likely not what you want.', gammas)); %#ok end if length(gammas) == 1 % Replicate to all three channels: gammas = [gammas, gammas, gammas]; end if nargin < 4 viewId = []; else viewId = varargin{3}; end % Retrieve all params for 'win'dow and given icmSpec, bind shader. The % 'icmSpec' string is empty - This will expand into the general 'ICM:' % string, so we use the first slot/shader with the ICM: token. icmId = 'SimpleGamma'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Set encoding gamma for power-law shader: uloc = glGetUniformLocation(glsl, 'ICMEncodingGamma'); if uloc >= 0 glUniform3f(uloc, gammas(1), gammas(2), gammas(3)); else error('Tried to set encoding gamma for color correction, but color correction not configured for use of encoding gamma!'); end catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); return; end if strcmpi(cmd, 'SetExtendedGammaParameters') % Need GL from here on... if isempty(GL) error('No internal GL struct defined in "SetExtendedGammaParameters" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('Must provide minimum input intensity value or vector of 3 minimum intensity values for R,G,B in 3rd argument!'); end if nargin < 4 error('Must provide maximum input intensity value or vector of 3 maximum intensity values for R,G,B in 4th argument!'); end if nargin < 5 error('Must provide output gain value or vector of 3 output gain values for R,G,B in 5th argument!'); end if nargin < 6 error('Must provide output bias value or vector of 3 output bias values for R,G,B in 5th argument!'); end % Fetch window handle: win = varargin{1}; % Fetch values: minL = varargin{2}; if ~isnumeric(minL) error('Minimum input intensity value(s) must be number(s)!'); end if length(minL)~=1 && length(minL)~=3 error('Minimum input intensity value must be a single scalar or a 3 component vector of separate values for the Red, Green and Blue color channel!'); end if length(minL) == 1 % Replicate to all three channels: minL = [minL, minL, minL]; end % Fetch values: maxL = varargin{3}; if ~isnumeric(maxL) error('Maximum input intensity value(s) must be number(s)!'); end if length(maxL)~=1 && length(maxL)~=3 error('Maximum input intensity value must be a single scalar or a 3 component vector of separate values for the Red, Green and Blue color channel!'); end if length(maxL) == 1 % Replicate to all three channels: maxL = [maxL, maxL, maxL]; end % Sanity check: if any((maxL - minL) <= 0) warning(sprintf('In at least one of the components of the provided minimum and maximum intensity vectors the provided\nminimum is *bigger or equal* than/to the maximum!\nThis will result in undefined behaviour and is likely not what you want.')); %#ok end % Compute reciprocal: recL = 1 ./ abs(maxL - minL); gain = varargin{4}; if ~isnumeric(gain) error('Output gain value(s) must be number(s)!'); end if length(gain)~=1 && length(gain)~=3 error('Output gain value must be a single scalar or a 3 component vector of separate values for the Red, Green and Blue color channel!'); end if length(gain) == 1 % Replicate to all three channels: gain = [gain, gain, gain]; end % Sanity check: if any(gain <= 0) warning(sprintf('At least one of the components of the provided gain vector is negative or zero!\nThis will result in undefined behaviour and is likely not what you want.')); %#ok end obias = varargin{5}; if ~isnumeric(obias) error('Output bias value(s) must be number(s)!'); end if length(obias)~=1 && length(obias)~=3 error('Output bias value must be a single scalar or a 3 component vector of separate values for the Red, Green and Blue color channel!'); end if length(obias) == 1 % Replicate to all three channels: obias = [obias, obias, obias]; end if nargin < 7 viewId = []; else viewId = varargin{6}; end % Retrieve all params for 'win'dow and given icmSpec, bind shader. The % 'icmSpec' string is empty - This will expand into the general 'ICM:' % string, so we use the first slot/shader with the ICM: token. icmId = 'SimpleGamma'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Set parameters for power-law shader: uloc = glGetUniformLocation(glsl, 'ICMMinInLuminance'); if uloc >= 0 glUniform3f(uloc, minL(1), minL(2), minL(3)); else error('Tried to set extended gamma parameters for color correction, but color correction not configured for use of extended gamma!'); end uloc = glGetUniformLocation(glsl, 'ICMMaxInLuminance'); if uloc >= 0 glUniform3f(uloc, maxL(1), maxL(2), maxL(3)); else error('Tried to set extended gamma parameters for color correction, but color correction not configured for use of extended gamma!'); end uloc = glGetUniformLocation(glsl, 'ICMReciprocalLuminanceRange'); if uloc >= 0 glUniform3f(uloc, recL(1), recL(2), recL(3)); else error('Tried to set extended gamma parameters for color correction, but color correction not configured for use of extended gamma!'); end uloc = glGetUniformLocation(glsl, 'ICMOutputGain'); if uloc >= 0 glUniform3f(uloc, gain(1), gain(2), gain(3)); else error('Tried to set extended gamma parameters for color correction, but color correction not configured for use of extended gamma!'); end uloc = glGetUniformLocation(glsl, 'ICMOutputBias'); if uloc >= 0 glUniform3f(uloc, obias(1), obias(2), obias(3)); else error('Tried to set extended gamma parameters for color correction, but color correction not configured for use of extended gamma!'); end catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); return; end if strcmpi(cmd, 'SetMultMatrix4') % Need GL from here on... if isempty(GL) error('SetMultMatrix4: No internal GL struct defined in "SetMultMatrix4" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('SetMultMatrix4: Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('SetMultMatrix4: Must provide 4-by-4 color transformation matrix.'); end % Fetch window handle: win = varargin{1}; % Fetch matrix: mat = varargin{2}; if ~isnumeric(mat) error('SetMultMatrix4: Matrix must contain numbers!'); end if size(mat,1)~=4 || size(mat,2)~=4 error('SetMultMatrix4: Matrix must have 4 rows and 4 columns!'); end if nargin < 4 viewId = []; else viewId = varargin{3}; end icmId = 'MatrixMultiply4'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Set 4x4 transform matrix to identity matrix by default: glUniformMatrix4fv(glGetUniformLocation(glsl, 'M'), 1, 0, single(mat)); catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); return; end if strcmpi(cmd, 'SetSensorToPrimary') % Need GL from here on... if isempty(GL) error('SetSensorToPrimary: No internal GL struct defined in "SetSensorToPrimary" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('SetSensorToPrimary: Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('SetSensorToPrimary: Must provide ''cal'' struct, just as for M-Function ''SensorToPrimary''.'); end % Fetch window handle: win = varargin{1}; % Fetch calibration struct: cal = varargin{2}; if ~isstruct(cal) error('SetSensorToPrimary: ''cal'' parameter must be a calibration struct, just as for M-Function ''SensorToPrimary''.'); end if ~isfield(cal, 'M_linear_device') || ~isfield(cal, 'ambient_linear') error('SetSensorToPrimary: ''cal'' struct is missing at least one of the required M_linear_device or ambient_linear fields.'); end % Extract the info we need and validate further (see SensorToPrimary for reference): M_linear_device = cal.M_linear_device; ambient_linear = cal.ambient_linear; if isempty(M_linear_device) || isempty(ambient_linear) || ~isnumeric(M_linear_device) || ~isnumeric(ambient_linear) error('SetSensorToPrimary has not been called on valid fields M_linear_device and ambient_linear in calibration structure.'); end % Ambient corrections [ma,na] = size(ambient_linear); if (ma ~= 3) || (na ~= 1) error('SetSensorToPrimary: Incorrect dimensions for ambient_linear. Must be a 3 element column vector.'); end % Color space conversion [mm,nm] = size(M_linear_device); if (mm ~= 3) || (nm ~= 3) error ('SetSensorToPrimary: Incorrect dimensions for M_linear_device. Must be a 3-by-3 color matrix.'); end % This is what we want to implement, with sensor being the (X,Y,Z) input % tristimulus color vector, and primary the output (R,G,B) color vector: % sensora = sensor - ambient_linear; % primary = M_linear_device * sensora; % % We do this by a 4x4 matrix vector multiplication: % % <=> primary = M_linear_device * (sensor - ambient_linear) % <=> primary = M_linear_device * sensor - M_linear_device * ambient_linear % <=> primary = M_linear_device * sensor - constC with constC = M_linear_device * ambient_linear % % <=> [ M_linear_device -constC ] with M_linear_device is upper 3x3 matrix embedded in 4 x 4 matrix % [ 0 0 0 1 ] and -constC embedded in right column of 4 x 4 matrix % % % This way the shader just needs to multiply our final matrix with the % input color vector. % Build constC: constC = M_linear_device * ambient_linear; % Build mother 4 x 4 matrix: mat = eye(4); % Start off as identity 4x4 matrix. mat(1:3, 1:3) = M_linear_device; % Embed upper 3x3 color matrix. mat(1:3, 4) = -constC; % Embed -constC 3 element vector. % mat is all we need (and love of course). if nargin < 4 viewId = []; else viewId = varargin{3}; end icmId = 'SensorToPrimary'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Set 4x4 transform matrix to identity matrix by default: glUniformMatrix4fv(glGetUniformLocation(glsl, 'M'), 1, 0, single(mat)); catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); return; end if strcmpi(cmd, 'SetLookupTable') % Need GL from here on... if isempty(GL) error('SetLookupTable: No internal GL struct defined in "SetLookupTable" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('SetLookupTable: Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('SetLookupTable: Must provide CLUT matrix for which a color lookup table should be built!'); end % Fetch window handle: win = varargin{1}; % Fetch clut: clut = varargin{2}; if ~isnumeric(clut) error('SetLookupTable: CLUT matrix must contain number(s)!'); end if size(clut,2)~=1 && size(clut,2)~=3 error('SetLookupTable: Encoding CLUT must be a one column luminance vector or three column matrix for the Red, Green and Blue color channel!'); end if size(clut,1) < 2 error('SetLookupTable: Encoding CLUT must have at least 2 rows, or at least 2 elements for a luminance vector!'); end if nargin < 4 viewId = []; else viewId = varargin{3}; end % Optional max input value provided? Assign most common 1.0 if not: if nargin < 5 || isempty(varargin{4}) ICMMaxInputValue = 1.0; else ICMMaxInputValue = varargin{4}; end % Optional scaling factor provided? Assign proper scaler for clut size, % and max input value if not: if nargin < 6 || isempty(varargin{5}) ICMPrescale = ( size(clut,1) -1 ) / ICMMaxInputValue; else ICMPrescale = varargin{5}; end % Retrieve all params for 'win'dow and our 'LookupTable' icmSpec, bind shader. icmId = 'LookupTable'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Setup initial clamping values to valid range 0.0 - maximum in passed CLUT: glUniform2f(glGetUniformLocation(glsl, 'ICMClampToColorRange'), 0.0, max(max(clut))); % Setup max input value and prescaler: glUniform1f(glGetUniformLocation(glsl, 'ICMMaxInputValue'),ICMMaxInputValue); glUniform1f(glGetUniformLocation(glsl, 'ICMPrescale'),ICMPrescale); catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); if isempty(icmDataForHandle) || size(icmDataForHandle, 1) < win || size(icmDataForHandle, 2) < glsl || Screen('WindowKind',win) ~= 1 error('SetLookupTable: Tried to assign clut to a non-onscreen window or one which doesn''t have "LookupTable" based color correction enabled!'); end % Convert 'clut' to single(), so it is a float format for OpenGL: clut = single(clut); % Try to encode in highest precision format that the hardware supports: winfo = Screen('GetWindowInfo', win); if winfo.GLSupportsTexturesUpToBpc >= 32 % Full 32 bits single precision float: internalFormat = GL.LUMINANCE_FLOAT32_APPLE; if verbosity >= 3, fprintf('PsychColorCorrection: Using a 32 bit float CLUT -> 23 bits effective linear output precision for color correction.\n'); end else % No float32 textures: if (winfo.GLSupportsTexturesUpToBpc >= 16) % Choose 16 bpc float textures: internalFormat = GL.LUMINANCE_FLOAT16_APPLE; if verbosity >= 3, fprintf('PsychColorCorrection: Using a 16 bit float CLUT -> 10 bits effective linear output precision for color correction.\n'); end else % No support for > 8 bpc textures at all and/or no need for % more than 8 bpc precision or range. Choose 8 bpc texture: internalFormat = GL.LUMINANCE; if verbosity >= 3, fprintf('PsychColorCorrection: Using a 8 bit integer CLUT -> 8 bits effective linear output precision for color correction.\n'); end % Plain old 8 bits fixed point: if (max(max(clut)) > 1) || (min(min(clut)) < 0) % Ohoh, out of range values for integer texture! if verbosity >= 2 fprintf('\nWARNING!PsychColorCorrection: CLUT contains values greater than 1.0 or negative values, which your hardware can''t handle!!\n'); fprintf('WARNING!PsychColorCorrection: This will likely cause remapping artifacts in color correction!!\n'); end end if (winfo.BitsPerColorComponent > 8) if verbosity >= 2 fprintf('WARNING!PsychColorCorrection: Your hardware can only handle 8 bit precision color correction in outputrange 0.0 - 1.0,\n'); fprintf('WARNING!PsychColorCorrection: but your framebuffer is configured for more than 8 bit precision. This may cause\n'); fprintf('WARNING!PsychColorCorrection: loss of effective precision in color correction and thereby unwanted artifacts!\n'); end end end end if size(clut,1) > glGetIntegerv(GL.MAX_RECTANGLE_TEXTURE_SIZE_ARB) error('SetLookupTable: Tried to assign a clut with %i slots. This is more than your graphics hardware can handle! [Maximum is %i slots].', size(clut,1), glGetIntegerv(GL.MAX_RECTANGLE_TEXTURE_SIZE_ARB)); end % Bind relevant texture object: glBindTexture(GL.TEXTURE_RECTANGLE_EXT, icmDataForHandle(win, glsl)); % Set filters properly: Want nearest neighbour filtering, ie., no filtering % at all. We'll do our own linear filtering in the ICM shader. This way % we can provide accelerated linear interpolation on all GPU's with all % texture formats, even if GPU's are old: glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_MIN_FILTER, GL.NEAREST); glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_MAG_FILTER, GL.NEAREST); % Want clamp-to-edge behaviour to saturate at minimum and maximum % intensity value, and to make sure that a pure-luminance 1 row clut is % properly "replicated" to all three color channels in rgb modes: glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); % Assign lookuptable data to texture: glTexImage2D(GL.TEXTURE_RECTANGLE_EXT, 0, internalFormat, size(clut, 1), size(clut, 2), 0, GL.LUMINANCE, GL.FLOAT, clut); glBindTexture(GL.TEXTURE_RECTANGLE_EXT, 0); % Done. return; end if strcmpi(cmd, 'SetLookupTable3D') % Need GL from here on... if isempty(GL) error('SetLookupTable3D: No internal GL struct defined in "SetLookupTable3D" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('SetLookupTable3D: Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('SetLookupTable3D: Must provide CLUT 4D matrix for which a 3D color lookup table should be built!'); end % Fetch window handle: win = varargin{1}; % Fetch clut: clut = varargin{2}; if ~isnumeric(clut) error('SetLookupTable3D: CLUT matrix must contain number(s)!'); end if ndims(clut) ~= 4 error('SetLookupTable3D: Encoding CLUT must be a 4D matrix with indices (channel, redinput, greeninput, blueinput)!'); end if size(clut,1)~=3 error('SetLookupTable3D: Encoding CLUT must be a 4D matrix whose first dimension has a size of 3 elements for (R,G,B) color triplets!'); end if size(clut,2) < 1 || size(clut,3) < 1 || size(clut,4) < 1 error('SetLookupTable3D: Encoding 4D CLUT must have at least one element in each of the three input color dimensions 2,3 and 4!'); end if nargin < 4 viewId = []; else viewId = varargin{3}; end % Optional max input value provided? Assign most common 1.0 if not: if nargin < 5 || isempty(varargin{4}) ICMMaxInputValue = 1.0; else ICMMaxInputValue = varargin{4}; end % Optional scaling factor provided? Assign proper scaler for normalized % 3D texture coordinate range of clut texture and max input value if not: if nargin < 6 || isempty(varargin{5}) ICMPrescale = 1.0 / ICMMaxInputValue; else ICMPrescale = varargin{5}; end % Optional precision spec provided? Assign if so. if nargin < 7 || isempty(varargin{6}) % Default to precision zero for simple 8 bit integer precision - % sufficient to drive a standard 8 bpc integer framebuffer. % Our strategy for 3D LUT's is different than for normal linear % LUT's, where we choose the highest precision (32 bpc float) by % default. The reason is that memory consumption of 3D textures % increases with O(n^3) for n levels per input dimension, as % opposed to O(n) for linear LUT's. Choosing higher precisions by % default could get us into a out-of-memory condition very quickly. % Even if the texture fits into VRAM, a 3D texture used for color % lookup and applied per pixel could quickly trash the texture % caches and cause a breakdown of performance. precision = 0; else precision = varargin{6}; end % Trilinear interpolation switch provided? if nargin < 8 || isempty(varargin{7}) % Default to trilinear interpolation: interpolate = 1; else interpolate = varargin{7}; end % Retrieve all params for 'win'dow and our 'LookupTable' icmSpec, bind shader. icmId = 'LookupTable3D'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok try % Setup initial clamping values to valid range 0.0 - maximum in passed CLUT: glUniform2f(glGetUniformLocation(glsl, 'ICMClampToColorRange'), 0.0, max(max(max(max(clut))))); % Setup max input value and prescaler: glUniform1f(glGetUniformLocation(glsl, 'ICMMaxInputValue'), ICMMaxInputValue); glUniform1f(glGetUniformLocation(glsl, 'ICMPrescale'), ICMPrescale); catch % Empty... psychrethrow(psychlasterror); end % Unbind shader: glUseProgram(0); if isempty(icmDataForHandle) || size(icmDataForHandle, 1) < win || size(icmDataForHandle, 2) < glsl || Screen('WindowKind',win) ~= 1 error('SetLookupTable3D: Tried to assign clut to a non-onscreen window or one which doesn''t have "LookupTable3D" based color correction enabled!'); end % Convert 'clut' to single(), so it is a float format for OpenGL: clut = single(clut); % Try to encode in highest precision format that the hardware supports, but no more than what is requested by usercode: winfo = Screen('GetWindowInfo', win); if (winfo.GLSupportsTexturesUpToBpc >= 32) && (winfo.GLSupportsFilteringUpToBpc >= 32 || interpolate == 0) && (precision >= 2) % Full 32 bits single precision float with linear filtering: internalFormat = GL.RGB_FLOAT32_APPLE; if verbosity >= 3, fprintf('PsychColorCorrection: Using a 32 bit float 3D CLUT -> 23 bits effective linear output precision for color correction.\n'); end else % No float32 textures with linear filtering: if (winfo.GLSupportsTexturesUpToBpc >= 16) && (winfo.GLSupportsFilteringUpToBpc >= 16 || interpolate == 0) && (precision >= 1) % Choose 16 bpc float textures with linear filtering: internalFormat = GL.RGB_FLOAT16_APPLE; if verbosity >= 3, fprintf('PsychColorCorrection: Using a 16 bit float 3D CLUT -> 10 bits effective linear output precision for color correction.\n'); end else % No support for > 8 bpc textures with linear filtering at all % and/or no need for more than 8 bpc precision or range. Choose % 8 bpc texture: internalFormat = GL.RGB; if verbosity >= 3, fprintf('PsychColorCorrection: Using a 8 bit integer 3D CLUT -> 8 bits effective linear output precision for color correction.\n'); end % Plain old 8 bits fixed point: if (max(max(max(max(clut)))) > 1) || (min(min(min(min(clut)))) < 0) % Ohoh, out of range values for integer texture! if verbosity >= 2 fprintf('\nWARNING!PsychColorCorrection: 3D CLUT contains values greater than 1.0 or negative values, which your hardware can''t handle!!\n'); fprintf('WARNING!PsychColorCorrection: This will likely cause remapping artifacts in color correction!!\n'); end end if (winfo.BitsPerColorComponent > 8) if verbosity >= 2 fprintf('WARNING!PsychColorCorrection: Your hardware can only handle 8 bit precision color correction in outputrange 0.0 - 1.0,\n'); fprintf('WARNING!PsychColorCorrection: but your framebuffer is configured for more than 8 bit precision. This may cause\n'); fprintf('WARNING!PsychColorCorrection: loss of effective precision in color correction and thereby unwanted artifacts!\n'); end end end end max3DSize = glGetIntegerv(GL.MAX_3D_TEXTURE_SIZE); if size(clut,2) > max3DSize || size(clut,3) > max3DSize || size(clut,4) > max3DSize error('SetLookupTable3D: Tried to assign a clut with (%i,%i,%i) slots. This is more than your graphics hardware can handle! [Maximum is %i slots].', size(clut,2), size(clut,3), size(clut,4), max3DSize); end % Bind relevant texture object: glBindTexture(GL.TEXTURE_3D, icmDataForHandle(win, glsl)); % Set filters properly: if interpolate > 0 % We want trilinear filtering: glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_MIN_FILTER, GL.LINEAR); glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_MAG_FILTER, GL.LINEAR); else % We want nearest neighbour lookup: glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_MIN_FILTER, GL.NEAREST); glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_MAG_FILTER, GL.NEAREST); end % Want clamp-to-edge behaviour to saturate at minimum and maximum % color values: glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); glTexParameteri(GL.TEXTURE_3D, GL.TEXTURE_WRAP_R, GL.CLAMP_TO_EDGE); % Clear any pending OpenGL errors: while glGetError(); end; % Assign lookuptable data to texture: glTexImage3D(GL.TEXTURE_3D, 0, internalFormat, size(clut, 2), size(clut, 3), size(clut, 4), 0, GL.RGB, GL.FLOAT, clut); % Check and handle errors: err = glGetError; if err ~= GL.NO_ERROR if err == GL.INVALID_VALUE % Can only get a invalid_value if power-of-two constraint is % violated on a GPU which does not support NPOT textures: error('SetLookupTable3D: LUT creation failed. Most likely cause: Your graphics card requires all 3D-CLUT dimensions to have a number of elements which is a power of two.\n'); elseif err == GL.OUT_OF_MEMORY error('SetLookupTable3D: LUT creation failed. Your graphics card has insufficient free memory to handle a CLUT of this size. Try to reduce CLUT size.'); else % Something else went bonkers: error('SetLookupTable3D: OpenGL error during lookup table texture creation: %s\n', gluErrorString(err)); end end % Done, LUT is ready, unbind: glBindTexture(GL.TEXTURE_3D, 0); % Done. return; end if strcmpi(cmd, 'SetGainMatrix') % Need GL from here on... if isempty(GL) error('SetGainMatrix: No internal GL struct defined in "SetGainMatrix" routine?!? This is a bug - Check code!!'); end if nargin < 2 error('SetGainMatrix: Must provide window handle to onscreen window as 2nd argument!'); end if nargin < 3 error('SetGainMatrix: Must provide 2D gain matrix for use!'); end % Fetch window handle: win = varargin{1}; % Fetch clut: clut = varargin{2}; if ~isnumeric(clut) error('SetGainMatrix: Gain matrix must contain number(s)!'); end [w, h] = Screen('WindowSize', win); if size(clut,2)~=w || size(clut,1)~=h error('SetGainMatrix: Gain matrix must have the same size (width x height) in pixels as the target onscreen window!'); end if nargin < 4 viewId = []; else viewId = varargin{3}; end if nargin < 5 precision = []; else precision = varargin{4}; end if isempty(precision) % Default precision for gain matrix is 2 == max. precision 32 bpc. precision = 2; end % Retrieve all params for 'win'dow and our 'LookupTable' icmSpec, bind shader. icmId = 'GainMatrix'; [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId); %#ok % % Not used yet: % try % % Setup initial clamping values to valid range 0.0 - maximum in passed CLUT: % glUniform2f(glGetUniformLocation(glsl, 'ICMClampToColorRange'), 0.0, max(max(clut))); % catch % % Empty... % psychrethrow(psychlasterror); % end % Unbind shader: glUseProgram(0); if isempty(icmDataForHandle) || size(icmDataForHandle, 1) < win || size(icmDataForHandle, 2) < glsl || Screen('WindowKind',win) ~= 1 error('SetGainMatrix: Tried to assign matrix to a non-onscreen window or one which doesn''t have "GainMatrix" based color correction enabled!'); end % Convert 'clut' to single(), so it is a float format for OpenGL. Also % need to transpose and flip from Matlab col-major to GL row-major: ch = size(clut, 3); for i=1:ch tclut(:,:,i) = transpose(flipud(clut(:,:,i))); %#ok end tclut = single(tclut); % Interleave RGB gain info if this is a 3 channel gain matrix: clut = zeros(size(tclut, 3), size(tclut, 1), size(tclut, 2), 'single'); for i=1:size(tclut, 3) clut(i,:,:) = tclut(:,:,i); end; % Try to encode in highest precision format that the hardware supports: winfo = Screen('GetWindowInfo', win); if (winfo.GLSupportsTexturesUpToBpc >= 32) && (precision >= 2) % Full 32 bits single precision float: internalFormat = GL.LUMINANCE_FLOAT32_APPLE; if ch == 3 internalFormat = GL.RGB_FLOAT32_APPLE; end if verbosity >= 3, fprintf('PsychColorCorrection: Using a 32 bit float matrix -> 23 bits (6 decimal digits) effective linear precision for color correction gain matrix.\n'); end else % No float32 textures: if (winfo.GLSupportsTexturesUpToBpc >= 16) && (precision >= 1) % Choose 16 bpc float textures: internalFormat = GL.LUMINANCE_FLOAT16_APPLE; if ch == 3 internalFormat = GL.RGB_FLOAT16_APPLE; end if verbosity >= 3, fprintf('PsychColorCorrection: Using a 16 bit float matrix -> 10 bits (3 decimal digits) effective linear precision for color correction gain matrix.\n'); end else % No support for > 8 bpc textures at all and/or no need for % more than 8 bpc precision or range. Choose 8 bpc texture: internalFormat = GL.LUMINANCE; if ch == 3 internalFormat = GL.RGB8; end if verbosity >= 3 fprintf('PsychColorCorrection: Using a 8 bit integer matrix -> 8 bits (2 decimal digits) effective linear precision for color correction gain matrix.\n'); fprintf('PsychColorCorrection: Gain values will be restricted to range 0.0 - 1.0, with 256 levels, ie. steps of 1/256.\n'); end % Plain old 8 bits fixed point: if (max(max(max(clut))) > 1) || (min(min(min(clut))) < 0) % Ohoh, out of range values for integer texture! if verbosity >= 2 fprintf('\nWARNING!PsychColorCorrection: Matrix contains values greater than one or negative values, which your hardware can''t handle!!\n'); fprintf('WARNING!PsychColorCorrection: This will likely cause remapping artifacts in gain correction!!\n'); end end if (winfo.BitsPerColorComponent > 8) if verbosity >= 2 fprintf('WARNING!PsychColorCorrection: Your hardware can only handle 8 bit precision gain correction width 256 discrete levels,\n'); fprintf('WARNING!PsychColorCorrection: but your framebuffer is configured for more than 8 bit precision. This may cause\n'); fprintf('WARNING!PsychColorCorrection: loss of effective precision in gain correction and thereby unwanted artifacts!\n'); end end end end % Bind relevant texture object: glBindTexture(GL.TEXTURE_RECTANGLE_EXT, icmDataForHandle(win, glsl)); % Set filters properly: Want nearest neighbour filtering, ie., no filtering at all: glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_MIN_FILTER, GL.NEAREST); glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_MAG_FILTER, GL.NEAREST); % Want clamp-to-edge behaviour to saturate at minimum and maximum gain value: glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); % Assign 2D matrix data to texture: if ch == 3 glTexImage2D(GL.TEXTURE_RECTANGLE_EXT, 0, internalFormat, size(clut, 2), size(clut, 3), 0, GL.RGB, GL.FLOAT, clut); else glTexImage2D(GL.TEXTURE_RECTANGLE_EXT, 0, internalFormat, size(clut, 2), size(clut, 3), 0, GL.LUMINANCE, GL.FLOAT, clut); end glBindTexture(GL.TEXTURE_RECTANGLE_EXT, 0); % Done. return; end error('Unknown subfunction specified. Typo?!? Read the help.'); function [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, viewId) % Fetch slot, and GLSL handle to relevant imaging slot and shader: if isempty(viewId) viewId = 'AllViews'; end % MK Resolved 26.4.2010: FIXME: HACK FOR BUG IN IMG PIPE!! if strcmpi(viewId, 'AllViews') || strcmpi(viewId, 'FinalFormatting') %if strcmpi(viewId, 'FinalFormatting') chain = 'FinalOutputFormattingBlit'; end % MK Resolved 26.4.2010: FIXME: HACK FOR BUG IN IMG PIPE!! if strcmpi(viewId, 'LeftView') if strcmpi(viewId, 'LeftView') %|| strcmpi(viewId, 'AllViews') chain = 'StereoLeftCompositingBlit'; end if strcmpi(viewId, 'RightView') chain = 'StereoRightCompositingBlit'; end if strcmpi(icmId, 'ClampedNoName') icmIdString = sprintf('CMIH:ClampedNoName'); else icmIdString = sprintf('ICM:%s', icmId); end [slot shaderid blittercfg voidptr glsl luttexid] = Screen('HookFunction', win, 'Query', chain, icmIdString); % Shader found? if slot == -1 % MK Resolved 26.4.2010: Doesn't hurt here. FIXME: HACK FOR BUG IN IMG PIPE!! if strcmpi(viewId, 'AllViews') % Special case 'AllViews': We searched 'LeftView' and failed. % Let's retry with 'FinalFormatting' view before we give up: [slot shaderid blittercfg voidptr glsl luttexid] = GetSlotForTypeAndBind(win, icmId, 'FinalFormatting'); % If we make it to this point then our search was successfull: return; end fprintf('PsychColorCorrection: Error: Failed to find plugin while searching chain: %s, searched icmIdString: %s\n', chain, icmIdString); error('Could not find shader plugin for color correction inside imaging pipeline for window and view! Is color correction really enabled for this window and view channel?!?'); end if glsl == 0 error('Color correction shader is not operational for unknown reason, maybe a Psychtoolbox bug? Sorry...'); end % Bind it: glUseProgram(glsl); return;