====== Spectral Light Modelling ====== These three core aspects of light simulation—global and local illumination models, and light sources—are the base for any light simulation. When it comes to spectral light simulations, specialized implementations of the aforementioned aspects are required, capable of simulating not only one or three light channels, as is typical for common light models, but also the entire light spectrum for different wavelengths. Note: The hardware requirement for performing GPU-based ray tracing is a programmable graphics card with OpenCL support. For example, any Nvidia card will do well, whereas older versions of integrated Intel cards—as they are often used in laptops—are not suitable for this. GPUFlux supports multiple GPU units and CPUs working in parallel at the same time. The use of multiple devices as well as the use of the CPU needs to be activated within the Preferences of the Flux renderer; see image below. {{ :tutorials:light_preferences.png?direct&400 |}} Spectral light simulations now deal not only with the pure calculation of light distributions but also include aspects of the principal characteristics of light, i.e., light quality, quantity, and duration. {{ :tutorials:light7.png?direct&400 |}} The main factor influencing the light quality is the light's spectral composition, commonly called colour. Thus, the compositions of different intensities of different wavelengths form the final light spectrum or colour. Below are the light spectra of typical sunlight, of common HPS lamps (high-pressure sodium lamps), as used for instance as additional light sources within greenhouses, and a red LED lamp. {{ :tutorials:light8.png?direct&500 |}} ==== Spectral light model - GPUFlux ==== In GroIMP, the GPUFlux model allows us to simulate spectral light between 380 and 720 nm (default values). Note: The implementation essentially allows setting individual limits for min and //maxLambda//, extending the range of the visible light spectrum to include infra-red and ultra-violet. When the wavelength deviates significantly from visible light, ray optics is no longer the most adequate tool for describing the flow of electromagnetic radiation, as other effects become significant. Therefore, from a physical point of view, the results will no longer be meaningful. One reason for this choice—the default range of 380-720 nm—is that the model uses Smith's conversion from RGB colours to full spectra. This conversion assumes that the whole spectrum is somewhat covered by the RGB colour, thus restricting the spectrum to the visual range. But if your model uses only spectral colours as inputs, then the interval could safely be extended. The spectral range //[minLambda, maxLambda]// can be divided into a user-defined number of equally-sized so-called buckets—sub-channels. They can be one, taking the whole range as one channel—which, in my opinion, would not make much sense—or can be as large as the number of integer wavelengths of the range, leading to 1 nm buckets if desired. Common numbers of buckets range from 5 to 30, but this totally depends on the application. Below is an example of a 380-720 nm spectrum divided into 20 buckets, each with a 17 nm range. {{ :tutorials:light9.png?direct&400 |}} Setting up the GPUFlux light model within GroIMP, or more accurately XL, follows the typical Java conventions of importing the required classes, and initializing and parameterizing the light model. Note: The examples require GroIPM version >=2.0 to run. With GroIMP version 2.0 some changes on the internal package structure are made. formally classes found in de.grogra.imp3d have been moved to de.grogra.gpuflux.imp3d to match the package name (Java 11 forbid package name split). So, if you are using objects, lights or shaders from gpuflux, they should be imported as de.grogra.gpuflux.imp3d.xxx. import de.grogra.imp3d.spectral.IrregularSpectralCurve; // old - before GroIMP 2.0 import de.grogra.gpuflux.imp3d.spectral.IrregularSpectralCurve; // new - with GroIMP 2.0 // Light nodes need to be imported like this import de.grogra.gpuflux.imp3d.objects.PhysicalLight; import de.grogra.gpuflux.tracer.FluxLightModelTracer.MeasureMode; ... const int RAYS = 10000000; //number of simulated light rays const int DEPTH = 10; //maxiaml recursion/reflection depth const FluxLightModel LM = new FluxLightModel(RAYS, DEPTH); protected void calculateLight() { LM.setMeasureMode(MeasureMode.FULL_SPECTRUM); // actual the default value LM.setSpectralBuckets(21); // get 20 buckets; set to N+1 to get N LM.setSpectralDomain(380, 720); // default settings too } The GPUFlux light model supports three different modes of measuring spectral power: * regular RGB, which simulates only three buckets approximating the three colour channels * fully discretized spectral measurements, the default mode, simulating spectral light * weighted integration, a weighted version of the spectral light simulation mode ∈ {RGB, FULL_SPECTRUM, INTEGRATED_SPECTRUM} The GPUFlux light model allows the following additional settings to control specifics: // sets the maximum negligible power quantum LM.setCutoffPower(0.01); // default: 0.001 // disables the simulation of sensor LM.setEnableSensors(true); // default: false // sets the random seed for the random number generator; us this to obtain reproducible results LM.setRandomseed(123456); // enable dispersion LM.setDispersion(ture); // default: false After the light model is configured, it can be invoked by calling the //compute()// function as follows: LM.compute(); To obtain the total amount of absorbed radiation of a node x, the //getAbsorbedPowerMeasurement// function of the light model needs to be called. The returned measurement object contains the results for the specific object x. By calling the //integrate()// function, the integral, or simply sum, will be calculated. Measurement spectrum = LM.getAbsorbedPowerMeasurement(x); float absorbedPower = spectrum.integrate(); By doing this within a rule, the light absorption can be obtained for all objects of the specified type, such as a //Box//, as in this example: [ x:Box ::> { Measurement spectrum = LM.getAbsorbedPowerMeasurement(x); float absorbedPower = spectrum.integrate(); } ] Accessing the absorption values for each bucket can be done by accessing the data variable of the //Measurement// class. Measurement spectrum = LM.getAbsorbedPowerMeasurement(x); // absorbed power for the first bucket: 380 -397nm float ap380_397 = spectrum.data[0]; // accumulate absorbed power into four buckets float b0 = 0, b1 = 0, b2 = 0, b3 = 0; for(int i:(0:4)) { b0 += spectrum.data[ 0+i]; // 0-4 b1 += spectrum.data[ 5+i]; // 5-9 b2 += spectrum.data[10+i]; // 10-14 b3 += spectrum.data[15+i]; // 15-19 } ==== Light sources ==== After the light model is set up, the next step is to define the spectral light sources. The GPUFlux light model works with all basic light nodes, such as //PointLight//, //SpotLight//, or //DirectionalLights//, but to fully realize the potential of spectral light modelling, it is necessary to define the emitted spectrum of the light source. The emitted spectrum can be defined as power intensities per wavelength, specifying the amplitude for specific wavelengths. Using the following spectrum will result in a dark magenta colour. WAVELENGTHS = {380, 410, 420, 450, 465, 480, 490, 600, 620, 630, 640, 655, 660, 670, 690, 700, 720}; AMPLITUDES = {0.05, 0.1, 0.4, 0.63, 0.25, 0.15, 0.05, 0.01, 0.1, 0.3, 0.4, 0.85, 0.75, 0.95, 0.6, 0.25, 0.1}; {{ :tutorials:light10.png?direct&400 |}} Note: The step size does not have to be equal, and values in between are linearly interpolated. The unit of the amplitudes is either given absolutely in watts or normalized between zero and one. The wavelength array is assumed to be sorted from low to high. A spectrum, given by an array of wavelengths and corresponding amplitudes, is called a spectral curve, and in computer graphics, it defines a spectral power distribution. In GroIMP, a spectral curve can be defined using the //IrregularSpectralCurve// class, which takes the wavelength array and the corresponding amplitudes as input. The //IrregularSpectralCurve// can be used as input to the //ChannelSPD// class so that it can later be used as input for the light node. const float[] WAVELENGTHS = {380, 410, 420, 450, 465, 480, 490, 600, 620, 630, 640, 655, 660, 670, 690, 700, 720}; const AMPLITUDES = {0.05, 0.1, 0.4, 0.63, 0.25, 0.15, 0.05, 0.01, 0.1, 0.3, 0.4, 0.85, 0.75, 0.95, 0.6, 0.25, 0.1}; const ChannelSPD TEST_SPD = new ChannelSPD(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)); Besides user-defined spectral curves, GroIMP provides a set of spectral curves: * BlackbodySpectralCurve * ConstantSpectralCurve * RegularSpectralCurve * RGBSpectralCurve * CIENormSpectralCurve {{ :tutorials:light_SpectralCurve.png?direct&200 |}} Since these spectral curve classes all implement the same //SpectralCurve// interface, they can be used in the same way and therefore exchanged. //user defined spectral curve, applied to an IrregularSpectralCurve float[] WAVELENGTHS = {380, 485, 490, 610, 615, 720}; float[] AMPLITUDES = {0,0,1,1,0,0}; ChannelSPD GREEN_SPD = new ChannelSPD(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)); //definition of a green SPD using a RGBSpectralCurve ChannelSPD GREEN_SPD = new ChannelSPD(new RGBSpectralCurve(0,1,0)); //a constant spectral curve of the intensity of 0.25 equally over the whole spectrum ChannelSPD CONST_SPD = new ChannelSPD(new ConstantSpectralCurve(0.25)); //a regular spectral curve will apply the given intensities over the specified range ([400,700]) ChannelSPD REG_SPD = new ChannelSPD(new RegularSpectralCurve(new float[] {0.1, 0.9,0.2,0.1,0.4}, 400, 700)); //a CIE Norm D55 spectral curve - sun light ChannelSPD REG_SPD = new ChannelSPD(new CIENormSpectralCurve(Attributes.CIE_NORM_D55)); // a black body spectral curve with a temperature of 5000K ChannelSPD REG_SPD = new ChannelSPD(new BlackbodySpectralCurve(5000)); {{ :tutorials:light11.png?direct&600 |}} To use the spectral curve as input for a light source, a //SpectralLight// needs to be defined. const float[] WAVELENGTHS = {380,385,...}; const float[] AMPLITUDES = {0.000967721, 0.000980455, ...}; module MyLamp extends LightNode() { { setLight( new SpectralLight(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)).( setPower(100), // [W] setLight(new PointLight()) ) // end SpectralLight ); // end setLight } } To complete the definition of a light source, besides the spectral power distribution, the physical light distribution (PLD), which defines the light pattern, needs to be defined. This is especially helpful or necessary for any definition of artificial light sources, such as those found in greenhouses, including HPS lamps or modern LED-based light systems. The physical light distribution can be defined as a polar distribution diagram (also called polar curve) showing the luminous intensity values with increasing angles from two imaginary axes of the lamp which is placed in the centre. Red: 0–180◦ plane, blue 90–270◦ plane. On the right of the Figure below, a 3D visualisation of the same light source is given. The colour of each point (gradient from black to bright red), as well as the distance to the light source, both indicate the power emitted by the light source in a particular direction per unit solid angle. {{ :tutorials:light_PLD.png?direct&500 |}} Within GroIMP, the PLD can be visualized for any light source, as illustrated below for a //SpotLight//. To activate the light ray visualization, the //setVisualize// function just needs to be set to true. Optionally, the number of visualized light rays and their length can be adjusted. protected void init () [ Axiom ==> LightNode.(setLight( new SpotLight().( setInnerAngle(0.02), setOuterAngle(0.7), setPower(100), setVisualize(true), //turn on the light ray visualization setNumberofrays(500), //set the number of visualized light rays setRaylength(2) //set the length of the visualized light rays ))); ] The result of the light ray visualization, i.e., the visualization of the physical light distribution, could look like the image below for different light sources: a) spotlight, with a defined opening angle; b) user-defined distribution; c) point light, equally distributed; d) directional light, equal distribution over an area. {{ :tutorials:light_visulizedLightSources.png?direct&500 |}} To see a more realistic light pattern, the scene needs to be rendered using one of the light models. Below is a rendered image of the LampDemo.gsz, as it can be found in the GroIMP internal example gallery. {{ :tutorials:light_lampDemo.png?direct&500 |}} Defining a PLD for a light source can be done in two ways: 1) 'manually' by defining the polar curve as an array of intensities within XL, or 2) by importing a PLD file—as it is provided by most professional light companies for their products. In any case, instead of one of the predefined light sources, such as //PointLight//, //SpotLight//, or //DirectionalLight//, a so-called //PhysicalLight// needs to be defined. The //PhysicalLight// allows us to apply a PLD to it. For the 'manual way,' the PLD is defined as a two-dimensional array (called DISTRIBUTION in the code snippet below) that is then passed as an input parameter to the //PhysicalLight// class. // definition of the PLD const double[][] DISTRIBUTION = { {131.25, 131.67, 132.37,...}, {131.36, 131.81, 132.11,...}, ... }; const float[] WAVELENGTHS = {380,385,...}; const float[] AMPLITUDES = {0.000967721, 0.000980455, ...}; module MyLamp extends LightNode() { { setLight( new SpectralLight(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)).( setPower(100), // [W] setLight(new PhysicalLight(DISTRIBUTION)) //here the PLD is used ) // end SpectralLight ); // end setLight } } ==== Local illumination - Shader ==== After the definition of the global illumination model and the light sources, the last missing part required for proper spectral light modelling is the definition of the local illumination model. In computer graphics, the tools used are called Shaders. A shader defines the local optical properties of an object, namely the values for reflection, absorption, and transmission. The Phong illumination model, or Phong shader for short, allows us to define all required aspects. In the same way as the spectral curves are defined for the light sources, the spectrum for reflectance and transmission needs to be defined for our spectral shader. The values for absorption are obtained as the 'remaining radiation', i.e., the difference between reflectance and transmission, when we subtract the reflectance and transmission from the total of incoming radiation: Absorption = Total - Reflectance - Transmission. Note: there is no check of plausibility implemented within the Phong shader. The user needs to make sure that the sum of reflectance and transmission is not higher than the actual incoming radiation. You cannot reflect or transmit more than what was incoming; otherwise, the object would be a light source emitting light. A Phong shader can be defined as following: float[] WAVELENGTHS = {380, 485, 490, 610, 615, 720}; float[] AMPLITUDES = {0,0,1,1,0,0}; ChannelSPD GREEN_SPD = new ChannelSPD(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)); ChannelSPD RED_SPD = new ChannelSPD(new RGBSpectralCurve(0.8,0,0)); ChannelSPD CONST_SPD = new ChannelSPD(new ConstantSpectralCurve(0.25)); Phong myShader = new Phong(); myShader.setDiffuse(GREEN_SPD); myShader.setTransparency(RED_SPD); //within a module that extends a Box module TestBox extends Box(0.001,1,1) { //define a variable of type Phong and call it myShader Phong myShader = new Phong(); //within the static constructor function - automatically called whenever a TestBox is generated // the shader is parametrized and applied to the TestBox { //define the shader aspects myShader.setDiffuse(GREEN_SPD); myShader.setTransparency(RED_SPD); myShader.setSpecular(CONST_SPD); //set the shader to the TestBox setShader(myShader); } } //as a module that is interpreted as Box module TestBox ==> { Phong myShader = new Phong(); } Box(0.001,1,1).(setShader(myShader)); Within the //CIENormSpectralCurve// class, a small database of several predefined standard spectral curves is given: * on 1nm resolution [300, 780] * A, D65 * on 5nm resolution [300, 780] * A, C, D50, D55, D65, D75 * on 5nm resolution [380, 780] * FL1-12, FL3_1-15, HP1-5 For instance, to use the predefined CIE NORM D65 for typical sun light within a user defined light module, one could use the following code. import de.grogra.imp3d.objects.Attributes; import de.grogra.gpuflux.imp3d.spectral.CIENormSpectralCurve; //define a light module module MyLamp extends LightNode { { setLight( new SpectralLight( new CIENormSpectralCurve(Attributes.CIE_NORM_D65) ).( setPower(100), //[W] setLight( new SpotLight(DISTRIBUTION).( setVisualize(true), //activate light ray visualization setNumberofrays(250), setRaylength(1)// [m] ) ) ) //end SpectralLight ); //end setLight } } Note: Do NOT mix common RGB shader like the //RGBAShader// with spectral shader! This applies for having RGB and spectral shader within the same model but in different objects but also for one (Phong) shader thas combines RGB and spectral shader. The ranges of common RGB shader will most probably not match the ranges of the other shader and simulated light spectrum what will inevitable lead to fals results. ==== Sensor nodes ==== To monitor light distributions with a scene without interfering, GroIMP provides the //SensorNode// class, a sphere that can be placed arbitrarily within the scene. To obtain the sensed spectrum, the //getSensedIrradianceMeasurement// function needs to be called. Note: The size of the sensor node directly correlates with the probability of got hit by a light ray. For a very small sphere the probability to got hit by a light ray is relatively low, so the number of light rays simulated by the light model needs to be much larger to get repayable results. Therefore, better not to use very small sensor nodes. Note: The colour of the sensor node determines which wavelengths should be observed. The default value is white, what stands for monitor all colours. If, for instance, the sensor colour is set to red, only red spectra will be sensed. Note: The output of a sensor node is normalized to absorbed radiance per square meter, independent of the actual size of the sensor. Note: Sensor nodes can be enabled and disabled for the light model using the LM.setEnableSensors(true/false) function. By default they are disabled, since GroIMP version 2.1.4, before they were enabled by default. Having them disabled speeds up the light computation time for scenarios where not sensor nodes are involved. // create a 5cm, white sensor node Axiom ==> SensorNode().(setRadius(0.05), setColor(1, 1, 1)); //check what the sensor node has sensed x:SensorNode ::> { Measurement spectrum = lm.getSensedIrradianceMeasurement(x); float absorbedPower = spectrum.integrate(); ... } ==== SPD and PLD files and references ==== Beside defining the SPD and PLD as arrays within XL, GroIMP supports the import of common file formates for both. Both can be imported in the same way by open the Panels Tab and go to Explorers first. For PLD, the 'Create new distribution object' needs to be called and for SPD 'Create new spectra object' is used. In the new panel, Object → New → file/spectrallightmapnode needs to be selected to get a file dialogue where the wanted file can be selected. After the file is imported, it is recommended to give the reference a self-explanatory name for easy use later within the code. {{ :tutorials:light_ref1.png?direct&300 |}} {{ :tutorials:light_ref7.png?direct&500 |}} To access the imported light spectra and physical light distributions within XL, one needs to define a reference to the files in the following way: //Definition of references const LightDistributionRef DISTRIBUTION = light(”distri1”); // for PLDs references const SpectrumRef SPECTRUM = spectrum(”equal”); // for SPDs references set them via the constructor module MyLamp extends LightNode { { setLight(new SpectralLight(new PhysicalLight(DISTRIBUTION),SPECTRUM, 5)); } } ==== Example ==== In the following four minimal working examples are given to illustrate: the light model, the definition of light sources, adding a object and define a spectral shader, and on how to visualize the results. The four examples are building on each other, meaning with each example new parts will extend the previous code. Note: The examples require GroIMP version >=2.0 to run. With GroIMP version 2.0 some changes on the internal package structure are made. formally classes found in //de.grogra.imp3d// have been moved to //de.grogra.gpuflux.imp3d// to match the package name (Java 11 forbid package name split). So, if you are using objects, lights or shaders from //gpuflux//, they should be imported as //de.grogra.gpuflux.imp3d.xxx//. import de.grogra.imp3d.spectral.IrregularSpectralCurve; // old - before GroIMP 2.0 import de.grogra.gpuflux.imp3d.spectral.IrregularSpectralCurve; // new - with GroIMP 2.0 // Light nodes need to be imported like this import de.grogra.gpuflux.imp3d.objects.PhysicalLight; === Example 1 - Light Model === This example just defines the GPUFlux light model and parameterizes it to simulate a spectrum from 300 to 800nm and measure the results in 30 buckets. import de.grogra.gpuflux.tracer.FluxLightModelTracer.MeasureMode; //constants for the light model: number of rays and maximal recursion depth const int RAYS = 1000000; const int DEPTH = 10; //initialize the scene protected void init () { //initialize the spectral light model println("Run GPU light model", 0x000000); FluxLightModel GPU_LM = new FluxLightModel(RAYS, DEPTH); GPU_LM.setSeed(12345); // to produce reproducible results GPU_LM.setMeasureMode(MeasureMode.FULL_SPECTRUM); GPU_LM.setSpectralDomain(300,800);// spectral range monitored GPU_LM.setSpectralBuckets(31);// range divided into 30 buckets GPU_LM.compute();// run the light model - may take a few seconds } The model will run and directly when saved, creates an instance of the light model, set wanted parameters and run it. there will be no output (except the one form the light model itself, stating that it was executed and giving some statistics on the scene). If you already get errors here, your system most probably does not support spectral light modelling. === Example 2 - Light Sources === This example defines a spectral light source with a user define physical light distribution (PLD) and a predefined CIE NORM D55 as spectral power distribution (SPD) (used to define typical sun light) and add the light source to the scene. import de.grogra.gpuflux.imp3d.spectral.CIENormSpectralCurve; import de.grogra.gpuflux.imp3d.spectral.IrregularSpectralCurve; import de.grogra.gpuflux.imp3d.objects.SpectralLight; import de.grogra.gpuflux.imp3d.objects.PhysicalLight; import de.grogra.gpuflux.tracer.FluxLightModelTracer.MeasureMode; import de.grogra.gpuflux.scene.experiment.Measurement; //////////////////////////////////////////////////////////////////////////////// //definition of a physical light distribution const double[][] DISTRIBUTION = { {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} }; //use the predefined CIE NORM D65 for typical sun light static const float[] WAVELENGTHS_S = CIENormSpectralCurve.NM_300_780_5; static const float[] AMPLITUDES_S = CIENormSpectralCurve.D65; //define a light node module MyLamp extends LightNode { { setLight( new SpectralLight( new IrregularSpectralCurve(WAVELENGTHS_S, AMPLITUDES_S) ).( setPower(7.8), //adapt the power to match the two curves setLight( new PhysicalLight(DISTRIBUTION).( setVisualize(true), //activate light ray visualization setNumberofrays(500), setRaylength(3.5) ) ) ) //end SpectralLight ); //end setLight } } //constants for the light model: number of rays and maximal recursion depth const int RAYS = 1000000; const int DEPTH = 10; //initialize the scene protected void init () { //create the actual 3D scene [ Axiom ==> MyLamp; ] //make sure the changes on the graph are applied... {derive();} //so that we directly can continue and work on the graph //initialize the spectral light model println("Run GPU light model", 0x000000); FluxLightModel GPU_LM = new FluxLightModel(RAYS, DEPTH); GPU_LM.setSeed(12345); // to produce reproduceable results GPU_LM.setMeasureMode(MeasureMode.FULL_SPECTRUM); GPU_LM.setSpectralDomain(300,800);// spectral range monitored GPU_LM.setSpectralBuckets(31);// range divided into 30 buckets GPU_LM.compute();// run the light model - may take a few seconds } Since the visualization of the light rays is turned on for the light source, we can see the light source in the 3D view window. {{ :tutorials:light_example1.png?direct&400 |}} === Example 3 - Scene object and shader === Here now we define a test object, s simple flat box of one square meter in dimension and apply a green spectral shader to it. The 3D view window should now show something similar to this: {{ :tutorials:light_example2.png?direct&400 |}} import de.grogra.gpuflux.imp3d.spectral.CIENormSpectralCurve; import de.grogra.gpuflux.imp3d.shading.ChannelSPD; import de.grogra.gpuflux.imp3d.spectral.IrregularSpectralCurve; import de.grogra.gpuflux.imp3d.objects.SpectralLight; import de.grogra.gpuflux.imp3d.objects.PhysicalLight; import de.grogra.gpuflux.tracer.FluxLightModelTracer.MeasureMode; import de.grogra.gpuflux.scene.experiment.Measurement; //////////////////////////////////////////////////////////////////////////////// //definition of a physical light distribution const double[][] DISTRIBUTION = { {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} }; //use the predefined CIE NORM D65 for typical sun light static const float[] WAVELENGTHS_S = CIENormSpectralCurve.NM_300_780_5; static const float[] AMPLITUDES_S = CIENormSpectralCurve.D65; //define a light node module MyLamp extends LightNode { { setLight( new SpectralLight( new IrregularSpectralCurve(WAVELENGTHS_S, AMPLITUDES_S) ).( setPower(7.8), //adapt the power to match the two curves setLight( new PhysicalLight(DISTRIBUTION).( setVisualize(true), //activate light ray visualization setNumberofrays(500), setRaylength(3.5) ) ) ) //end SpectralLight ); //end setLight } } //////////////////////////////////////////////////////////////////////////////// //define a green shader as user-defined irregular spectral curve public const float[] WAVELENGTHS = {300, 525, 530, 575, 580, 800}; public const float[] AMPLITUDES = {0,0,1,1,0,0}; const ChannelSPD GREEN_SPD = new ChannelSPD(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)); //apply the shader to an object: a box of one square meter module TestShader ==> { Phong myShader = new Phong(); //myShader.setDiffuse(new RGBColor (0,1,0)); myShader.setDiffuse(GREEN_SPD); } Box(0.001,1,1).(setShader(myShader)); //////////////////////////////////////////////////////////////////////////////// //constants for the light model: number of rays and maximal recursion depth const int RAYS = 1000000; const int DEPTH = 10; //initialize the scene protected void init () { clearConsole(); //create the actual 3D scene [ Axiom ==> TestShader M(2) RL(180) MyLamp; ] //make sure the changes on the graph are applied... {derive();} //so that we directly can continue and work on the graph //initialize the spectral light model println("Run GPU light model", 0x000000); FluxLightModel GPU_LM = new FluxLightModel(RAYS, DEPTH); GPU_LM.setSeed(12345); // to produce reproduceable results GPU_LM.setMeasureMode(MeasureMode.FULL_SPECTRUM); GPU_LM.setSpectralDomain(300,800);// spectral range monitored GPU_LM.setSpectralBuckets(31);// range divided into 30 buckets GPU_LM.compute();// run the light model - may take a few seconds //check the scene objects for their light absorption Measurement ms; [ x:TestShader ::> { ms = GPU_LM.getAbsorbedPowerMeasurement(x); } ] println(""+ms.integrate()+" = "+ms, 0xff0000); } To obtain the measurement results, one needs to first run the light model and second check each (wanted) scene object for its absorption values. A simple graph query can eb used to implement the second part, where here is searched for all TestShader instances within the graph and the light absorption is obtained. Afterwards the results a printed to the GroIMP console window. The output of the code is the integrated absorbed power and the array of the absorption values for each bucket. {{ :tutorials:light_example3.png?direct&400 |}} === Example 4 - Output visualization === In the final version, we are now going to add a charts to visualize the emitted spectrum and to plot it against the absorbed spectrum of the test object. {{ :tutorials:light_example4.png?direct&400 |}} import de.grogra.gpuflux.imp3d.spectral.CIENormSpectralCurve; import de.grogra.gpuflux.imp3d.shading.ChannelSPD; import de.grogra.gpuflux.imp3d.spectral.IrregularSpectralCurve; import de.grogra.gpuflux.imp3d.objects.SpectralLight; import de.grogra.gpuflux.imp3d.objects.PhysicalLight; import de.grogra.gpuflux.tracer.FluxLightModelTracer.MeasureMode; import de.grogra.gpuflux.scene.experiment.Measurement; //////////////////////////////////////////////////////////////////////////////// //definition of a physical light distribution const double[][] DISTRIBUTION = { {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} }; //use the predefined CIE NORM D65 for typical sun light static const float[] WAVELENGTHS_S = CIENormSpectralCurve.NM_300_780_5; static const float[] AMPLITUDES_S = CIENormSpectralCurve.D65; //define a light node module MyLamp extends LightNode { { setLight( new SpectralLight( new IrregularSpectralCurve(WAVELENGTHS_S, AMPLITUDES_S) ).( setPower(7.8), //adapt the power to match the two curves setLight( new PhysicalLight(DISTRIBUTION).( setVisualize(true), //activate light ray visualization setNumberofrays(500), setRaylength(3.5) ) ) ) //end SpectralLight ); //end setLight } } //////////////////////////////////////////////////////////////////////////////// //define a green shader as user-defined irregular spectral curve public const float[] WAVELENGTHS = {300, 525, 530, 575, 580, 700}; public const float[] AMPLITUDES = {0,0,1,1,0,0}; const ChannelSPD GREEN_SPD = new ChannelSPD(new IrregularSpectralCurve(WAVELENGTHS, AMPLITUDES)); //apply the shader to an object: a box of one square meter module TestShader ==> { Phong myShader = new Phong(); //myShader.setDiffuse(new RGBColor (0,1,0)); myShader.setDiffuse(GREEN_SPD); } Box(0.001,1,1).(setShader(myShader)); //////////////////////////////////////////////////////////////////////////////// //define the data sheet const DatasetRef absorbedChart = new DatasetRef("Power [W]"); //constants for the light model: number of rays and maximal recursion depth const int RAYS = 1000000; const int DEPTH = 10; //initialize the scene protected void init () { clearConsole(); //initialize the chart absorbedChart.clear().setColumnKey(0,"source emitted").setColumnKey(1,"ground absorbed"); chart(absorbedChart, XY_PLOT); //plot the emitted spectral curve float INTEGRAL = 0; for(int i:(0:AMPLITUDES_S.length-1)) INTEGRAL+=AMPLITUDES_S[i]; for(int i:(0:WAVELENGTHS_S.length-1)) { absorbedChart.addRow().(set(0, WAVELENGTHS_S[i], AMPLITUDES_S[i]/INTEGRAL)); } //create the actual 3D scene [ Axiom ==> TestShader M(2) RL(180) MyLamp; ] //make sure the changes on the graph are applied... {derive();} //so that we directly can continue and work on the graph //initialize the spectral light model println("Run GPU light model", 0x000000); FluxLightModel GPU_LM = new FluxLightModel(RAYS, DEPTH); GPU_LM.setSeed(12345); // to produce reproduceable results GPU_LM.setMeasureMode(MeasureMode.FULL_SPECTRUM); GPU_LM.setSpectralDomain(300,800);// spectral range monitored GPU_LM.setSpectralBuckets(31);// range divided into 30 buckets GPU_LM.compute();// run the light model - may take a few seconds //check the scene objects for their light absorption Measurement ms; [ x:TestShader ::> { ms = GPU_LM.getAbsorbedPowerMeasurement(x); } ] print("absorbed = "+ms);println(""+ms.integrate()+" = "+ms, 0xff0000); //plot the absorption spectrum for(int i:(0:ms.data.length-1)) { absorbedChart.addRow().(set(1, 300+i*16.129, ms.data[i])); //500/31=16.129 } } ===== Acknowledgements ===== Special thanks to Dietger van Antwerpen, who implemented the GPUFlux light model for GroIMP! ===== References ===== * Henke M and Buck-Sorlin GH (2018) Using a full spectral raytracer for the modelling of light microclimate in a functional-structural plant model; Computing and Informatics, 36(6), 1492-1522, doi: 10.4149/cai_2017_6_1492, [[https://www.cai.sk/ojs/index.php/cai/article/view/2017_6_1492]] * van Antwerpen, D.G. (2011) High Performance Spectral Light Transport Model for Agricultural Applications, Poster HPG * van Antwerpen, D.G. (2011) Unbiased physically based rendering on the GPU, Master thesis, Delft University of Technology