[UWP][Lighting]Spotlight Query

RobDev 401 Reputation points
2021-09-24T14:15:50.547+00:00

Greetings All,

I'm developing a UWP circular progress bar user control (yes, I know there's already one available but I wanted to have a go myself). So far everything has been pretty straight forward upto the point where I thought I'd illuminate my progress bar as it travelled round with a spotlight. This is a problem that I need help with.

First of all, let me state that the progress bar consists of two concentric circles that contain the circle that describes the progress so far. The two containing circles have a 'thickness' of 5 each and the circle that describes the progress has a 'thickness' of 20, as shown in the following diagram:

135067-diag1.png

I've positioned the spotlight at the exact middle of the progress circle's width and at its leading edge and I've given the Z vector value at 50. So I'm trying to calculate the spotlight's 'InnerConeAngle' value - I'm setting its 'OuterConeAngle' to the same value. This should project the spotlight exactly onto the progress bar with no overlap.

To calculate the angle between the outer circle edge, the spotlight and the middle of the progress circle's width I calculated the tangent of that angle by dividing its opposite, the distance between the middle of the progress circle width and the outer circle edge (10 + 5 = 15) by its adjacent, the distance between the spotlight's Z position and the middle of the progress circle (50): 15 / 50 = 0.3 approx. When I plug this value into the atan() function and multiply the result by 180/Math.PI to get degrees I get a result of 16.6 approx. If I repeat this process with the inner circle and add the results together I should get the required angle for my spotlight. In this instance, as the inner and outer circles are the same 'thickness', my result will be 16.6 + 16.6 = 33.2. However, when this result is used as the spotlight's 'InnerConeAngle' and 'OuterConeAngle' I get the result shown in the following diagram:

135049-diag2.png

As can be seen, the spotlight's light cone is way over size. So, my question is: what did I do wrong?

Any help gratefully received.

Universal Windows Platform (UWP)
{count} votes

1 answer

Sort by: Most helpful
  1. RobDev 401 Reputation points
    2021-10-01T14:19:12.313+00:00

    Hi @Nico Zhu (Shanghai Wicresoft Co,.Ltd.) ,

    I decided to send a 'dummy' answer rather than use Github or OneDrive, so here it is:

    The circular progress bar is implemented as a User Control in a UWP project. The Xaml for it is as follows:

        <UserControl  
            x:Class="UWPCircTest1.RoundProgressControl"  
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
            xmlns:local="using:UWPCircTest1"  
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
            mc:Ignorable="d"  
            d:DesignHeight="300"  
            d:DesignWidth="400" Loaded="UserControl_Loaded" SizeChanged="UserControl_SizeChanged">  
          
          
            <Grid Name="TheGrid">  
                <Grid.Lights>  
                    <local:TestAmbientLight/>  
                </Grid.Lights>  
                <Path x:Name="ThePath" Fill="Transparent" Stroke="Green" StrokeThickness="20" StrokeDashCap="Flat">  
                    <Path.Data>  
                        <PathGeometry>  
                            <PathGeometry.Figures>  
                                <PathFigureCollection>  
                                    <PathFigure x:Name="TheFigure" StartPoint="0,0">  
                                        <PathFigure.Segments>  
                                            <PathSegmentCollection>  
                                                <ArcSegment x:Name="TheSegment" IsLargeArc="False" SweepDirection="Clockwise" Point="0,0" />  
                                            </PathSegmentCollection>  
                                        </PathFigure.Segments>  
                                    </PathFigure>  
                                </PathFigureCollection>  
                            </PathGeometry.Figures>  
                        </PathGeometry>  
                    </Path.Data>  
                </Path>  
                <TextBlock x:Name="ValInd" HorizontalAlignment="Center" VerticalAlignment="Center" Text="Test %"/>  
                <Ellipse x:Name="OuterCirc" Fill="Transparent" Width="50" Height="50" StrokeThickness="5" Stroke="DodgerBlue"/>  
                <Ellipse x:Name="InnerCirc" Fill="Transparent" Width="40" Height="40" StrokeThickness="5" Stroke="DodgerBlue"/>  
            </Grid>  
        </UserControl>  
    

    The 'code behind' does contain a bit of irrelevant debug info but is as follows:

            public sealed partial class RoundProgressControl : UserControl  
            {  
                bool IsDataInitiated;  
                public double MaxValue { get; set; }  
                public double MinValue { get; set; }  
          
                private double lastValue;  
          
                private double gridWidth;  
                private double gridHeight;  
                private Point gridCentre;  
                private double circ3Rad;  
                private double rad3X;  
                private double rad3Y;  
                private Point _startPoint;  
                private Point _endPoint;  
                private Size _circSize;  
          
          
                private double orgGridWidth;  
                private double orgGridHeight;  
          
                private SpotLight myLight;  
          
                private const float SpotLightZVal = 50;  
                public RoundProgressControl()  
                {  
                    this.InitializeComponent();  
                    IsDataInitiated = false;  
                }  
          
                public void InitInternalData()  
                {  
                    gridWidth = orgGridWidth = TheGrid.ActualWidth;  
                    gridHeight = orgGridHeight = TheGrid.ActualHeight;  
                    gridCentre.X = gridWidth / 2;  
                    gridCentre.Y = gridHeight / 2;  
                    rad3X = gridWidth / 4.0;  
                    rad3Y = gridHeight / 4.0;  
                    circ3Rad = (rad3X < rad3Y ? rad3X : rad3Y);  
                    _startPoint = new Point(gridCentre.X, gridCentre.Y - circ3Rad);  
                    _endPoint = new Point(_startPoint.X, _startPoint.Y);  
                    _circSize = new Size(circ3Rad, circ3Rad);  
                    TheFigure.StartPoint = _startPoint;  
                    TheSegment.Point = _endPoint;  
                    TheSegment.Size = _circSize;  
                    lastValue = 0.0;  
          
                    OuterCirc.Height = (circ3Rad * 2.0) + ThePath.StrokeThickness;  
                    OuterCirc.Width = (circ3Rad * 2.0) + ThePath.StrokeThickness;  
          
                    InnerCirc.Height = (circ3Rad * 2.0) - ThePath.StrokeThickness;  
                    InnerCirc.Width = (circ3Rad * 2.0) - ThePath.StrokeThickness;  
          
                    var gridVisual = ElementCompositionPreview.GetElementVisual(TheGrid);  
          
                    var compositor = gridVisual.Compositor;  
                    this.myLight = compositor.CreateSpotLight();  
                    this.myLight.CoordinateSpace = gridVisual;  
                    this.myLight.InnerConeColor = Colors.White;  
          
                    // Not Quite right  
                    double opp = ((ThePath.StrokeThickness / 2.0) + OuterCirc.StrokeThickness);  
                    double adj = SpotLightZVal;  
                    double tanOppAdj = opp / adj;  
                    double radTanOppAdj = Math.Atan(tanOppAdj);  
                    double angTanOppAdj = radTanOppAdj * (180 / Math.PI);  
          
                    double outerBit = Math.Atan(((ThePath.StrokeThickness / 2.0) + OuterCirc.StrokeThickness) / SpotLightZVal) * (180 / Math.PI);  
                    double innerBit = Math.Atan(((ThePath.StrokeThickness / 2.0) + InnerCirc.StrokeThickness) / SpotLightZVal) * (180 / Math.PI);  
          
                    this.myLight.InnerConeAngleInDegrees = (float)(outerBit + innerBit);  
                    this.myLight.OuterConeAngleInDegrees = (float)(outerBit + innerBit);  
          
          
          
                    this.myLight.Offset = new Vector3((float)_startPoint.X, (float)_startPoint.Y, SpotLightZVal);  
                    this.myLight.Targets.Add(gridVisual);  
          
          
                    IsDataInitiated = true;  
                }  
          
                private void UserControl_Loaded(object sender, RoutedEventArgs e)  
                {  
                    InitInternalData();  
                }  
          
                public void SetCircLength(double value)  
                {  
                    StringBuilder sb = new StringBuilder();  
          
                    //double realValue = newValue - oldValue;  
                    lastValue = value;  
                    double angle = 2 * Math.PI * (value / MaxValue);  
          
                    //sb.AppendFormat("value = {0}  -  angle = {1}", value, angle);  
                    //System.Diagnostics.Debug.WriteLine(sb.ToString());  
          
                    double X = gridCentre.X + (Math.Sin(angle) * circ3Rad);  
                    double Y = gridCentre.Y - (Math.Cos(angle) * circ3Rad);  
          
                    if (value > 0)  
                    {  
                        //  Math.Round() is producing the wrong number - there's a tiny difference between X and _startPoint.X and  
                        //  it's making the difference big by cpmparison. Sort this out!! -  Sorted!!  
                        //  Check if circle is about to be complete - Circle disappears if start point == end point  
                        int x1 = (int)Math.Round(X, 5, MidpointRounding.AwayFromZero);  
                        int x2 = (int)Math.Round(_startPoint.X, 5, MidpointRounding.AwayFromZero);  
                        int y1 = (int)Math.Round(Y, 5, MidpointRounding.AwayFromZero);  
                        int y2 = (int)Math.Round(_startPoint.Y, 5, MidpointRounding.AwayFromZero);  
                        if (x1 == x2 && y1 == y2)  
                        {  
                            X -= 0.01;  //  Was +=, Now -= reduces X by 0.01  
                        }  
                    }  
          
                    // Run this on the UI thread because the IsLargeArc and Point values need to get set only in that thread.  
                    IAsyncAction TheTask =  
                        CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High,  
                        () =>  
                        {  
                            _endPoint.X = X;  
                            _endPoint.Y = Y;  
                            TheSegment.IsLargeArc = angle >= 3.14159265;  
                            TheSegment.Point = new Point(X, Y);  
                            sb.AppendFormat("{0}%", CalcPercent(value / MaxValue));  
                            ValInd.Text = sb.ToString();  
                            myLight.Offset = new Vector3((float)X, (float)Y, myLight.Offset.Z);  
                        });  
                }  
          
                private double CalcPercent(double _val)  
                {  
                    double retVal = (_val / 1.00 * 100);  
                    return retVal;  
                }  
          
                private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)  
                {  
                    double c3Rad;  
                    Size c3Size;  
                    double r3X;  
                    double r3Y;  
          
          
                    StringBuilder sb = new StringBuilder();  
          
                    sb.AppendFormat("Centre = {0}, {1}   Size = Width {2} , Height {3}",  
                        gridCentre.X,gridCentre.Y,  
                        TheSegment.Size.Width, TheSegment.Size.Height);  
                    System.Diagnostics.Debug.WriteLine(sb.ToString());  
          
                    if (IsDataInitiated)  
                    {  
                        gridWidth = TheGrid.ActualWidth;  
                        gridHeight = TheGrid.ActualHeight;  
                        gridCentre.X = gridWidth / 2.0;  
                        gridCentre.Y = gridHeight / 2.0;  
                        r3X = gridWidth / 4.0;  
                        r3Y = gridHeight / 4.0;  
                        c3Rad = r3X < r3Y ? r3X : r3Y;  
                        c3Size = new Size(c3Rad, c3Rad);  
          
                        _circSize = c3Size;  
                        circ3Rad = c3Rad;  
                        TheSegment.Size = _circSize;  
          
                        if (_startPoint == _endPoint)  
                        {  
                            _startPoint = new Point(gridCentre.X, gridCentre.Y - circ3Rad);  
                            _endPoint = new Point(_startPoint.X, _startPoint.Y);  
          
                        }  
                        else  
                        {  
                            _startPoint = new Point(gridCentre.X, gridCentre.Y - circ3Rad);  
                            SetCircLength(lastValue);  
                        }  
          
                        TheFigure.StartPoint = _startPoint;  
                        TheSegment.Point = _endPoint;  
                        myLight.Offset = new Vector3((float)_endPoint.X, (float)_endPoint.Y, myLight.Offset.Z);  
                        OuterCirc.Height = (circ3Rad * 2.0) + ThePath.StrokeThickness;  
                        OuterCirc.Width = (circ3Rad * 2.0) + ThePath.StrokeThickness;  
                        InnerCirc.Height = (circ3Rad * 2.0) - ThePath.StrokeThickness;  
                        InnerCirc.Width = (circ3Rad * 2.0) - ThePath.StrokeThickness;  
                    }  
          
                }  
            }  
    

    The control was contained within a test harness that utilised a slider control to provide the progress control's data. Whenever the slider's value changed its 'value changed' handler is called which, in turn' calls the SetCircLength() function on the progress control.

    I think this covers everything I've done. As you can see it's all pretty straight forward. it's just the spotlight cone angle I can't get right.