3D Surface Rendering with User-Defined Mathematical Functions in ASP.NET

Mathematical Background

In three-dimensional space, a point consists of three floating-point coordinates (x, y, z) representing a specific location. The R² surface within R³ functions is defined by expressions of the form z = f(x, y), where x and y belong to a defined subset of the function's domain. The R² domain corresponds to the XY plane in Cartesian coordinates.

The objective involves generating a collection of z-coordinates by applying a general function z = f(x, y) to a discrete domain—specifically, applying the function to a matrix of defined x and y points that form a grid on the image plane.

Domain Definition and Discretization

To perform calculations, begin by establishing the computational domain. For instance, define x within the interval [-1.7, 1.3] and y within the interval [-2.1, 2.5]. This means x progresses from -1.7 to 1.3 while y progresses from -2.1 to 2.5.

Since working with real coordinates means having infinite points between any two coordinate values, introduce a small parameter called the "step size" to traverse the interval. The algorithm walks between coordinates using this step size to generate the grid points.

double xStart = -1.7, xEnd = 1.3, yStart = -2.1, yEnd = 2.5;
double stepSize = 0.1;
double x = xStart;

while (x <= xEnd)
{
    double y = yStart;
    while (y <= yEnd)
    {
        // Process each grid point
        y += stepSize;
    }
    x += stepSize;
}

Computing Surface Coordinates

Building upon the domain definition, compute z-coordinates by evaluating the function z = f(x, y) across the discrete XY subdomain:

double xStart = -1.7, xEnd = 1.3, yStart = -2.1, yEnd = 2.5;
double stepSize = 0.1;
double x = xStart;
double z;

while (x <= xEnd)
{
    double y = yStart;
    while (y <= yEnd)
    {
        z = EvaluateFunction(x, y);  // Calculate z for given x, y
        // Store coordinates for rendering in subsequent step
        y += stepSize;
    }
    x += stepSize;
}

Dynamic Template Generation

Rather than using a mathematical expression parser with string-based formulas, this approach employs template file manipulation with replacement tags for simplicity.

The workflow is straightforward:

  1. Read the template file containing the function placeholder
  2. Replace the placeholder with the user-supplied expression
  3. Write the generated ASPX file to render the visualization

Important: Ensure write permissions are granted for the target directory.

string templateContent = ReadFile(Server.MapPath("./template.txt"));
templateContent = templateContent.Replace("@FORMULA", userExpression);
SaveFile(Server.MapPath("./output.aspx"), templateContent);

Template File Implementation

<%@ Page Language="c#" Debug="true" Explicit="True" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="System.Globalization" %>

<script language="C#" runat="server">

    void RotateAroundXAxis(ref double coordY, ref double coordZ, double radians)
    {
        double tempY = coordY * Math.Cos(radians) - coordZ * Math.Sin(radians);
        double tempZ = coordY * Math.Sin(radians) + coordZ * Math.Cos(radians);
        coordY = tempY;
        coordZ = tempZ;
    }

    void RotateAroundYAxis(ref double coordX, ref double coordZ, double radians)
    {
        double tempX = coordX * Math.Cos(radians) - coordZ * Math.Sin(radians);
        double tempZ = coordX * Math.Sin(radians) + coordZ * Math.Cos(radians);
        coordX = tempX;
        coordZ = tempZ;
    }

    void RotateAroundZAxis(ref double coordX, ref double coordY, double radians)
    {
        double tempX = coordX * Math.Cos(radians) - coordY * Math.Sin(radians);
        double tempY = coordX * Math.Sin(radians) + coordY * Math.Cos(radians);
        coordX = tempX;
        coordY = tempY;
    }

    protected double ComputeSurfaceValue(double x, double y)
    {
        return (@FORMULA);
    }

    public void WriteToFile(string filePath, string content)
    {
        using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
        }
    }

    private void Page_Load(object sender, EventArgs e)
    {
        var culture = (CultureInfo)Thread.CurrentThread.CurrentCulture.Clone();
        culture.NumberFormat.NumberDecimalSeparator = ".";
        Thread.CurrentThread.CurrentCulture = culture;

        int pixels = Session["pixels"] != null ? (int)Session["pixels"] : 200;
        double xMin = Session["xMin"] != null ? (double)Session["xMin"] : -2;
        double xMax = Session["xMax"] != null ? (double)Session["xMax"] : 2;
        double yMin = Session["yMin"] != null ? (double)Session["yMin"] : -2;
        double yMax = Session["yMax"] != null ? (double)Session["yMax"] : 2;
        double stepVal = Session["stepVal"] != null ? (double)Session["stepVal"] : 0.05;


        if (Math.Abs(stepVal) < 1.0E-5)
        {
            Response.Write("Step value is zero");
            return;
        }

        const int maxPoints = 1000000;

        int columns = (int)Math.Ceiling((xMax - xMin) / stepVal) + 1;
        int rows = (int)Math.Ceiling((yMax - yMin) / stepVal) + 1;
        int totalPoints = columns * rows;

        if (totalPoints > maxPoints)
        {
            Response.Write("Point limit exceeded");
            return;
        }

        double[,] surfaceData = new double[columns, rows];

        double x = xMin;
        int colIndex = 0;
        double zMin = +1.0E10, zMax = -1.0E10;

        while (x <= xMax)
        {
            double y = yMin;
            int rowIndex = 0;
            while (y <= yMax)
            {
                surfaceData[colIndex, rowIndex] = ComputeSurfaceValue(x, y);
                zMin = Math.Min(zMin, surfaceData[colIndex, rowIndex]);
                zMax = Math.Max(zMax, surfaceData[colIndex, rowIndex]);
                rowIndex++;
                y += stepVal;
            }
            colIndex++;
            x += stepVal;
        }

        if (Math.Abs(zMin) < 1.0E-5) zMin = 0;
        if (Math.Abs(zMax) < 1.0E-5) zMax = 0;

        Session["surfaceData"] = surfaceData;

        using (var bitmap = new Bitmap(pixels, pixels, PixelFormat.Format32bppArgb))
        using (var graphics = Graphics.FromImage(bitmap))
        {
            graphics.FillRectangle(Brushes.Black, 0, 0, pixels, pixels);

            double eyeX = Session["eyeX"] != null ? (double)Session["eyeX"] : 0;
            double eyeY = Session["eyeY"] != null ? (double)Session["eyeY"] : 0;
            double eyeZ = Session["eyeZ"] != null ? (double)Session["eyeZ"] : 100;

            int idx = 0;
            x = xMin;

            while (x < xMax && idx < columns - 1)
            {
                double y = yMin;
                int idy = 0;

                while (y < yMax && idy < rows - 2 && idx < columns - 1)
                {
                    double z1 = surfaceData[idx, idy];
                    double z2 = surfaceData[idx + 1, idy];
                    double z3 = surfaceData[idx + 1, idy + 1];
                    double z4 = surfaceData[idx, idy + 1];

                    double px1 = x, py1 = y, pz1 = z1;
                    double px2 = x + stepVal, py2 = y, pz2 = z2;
                    double px3 = x + stepVal, py3 = y + stepVal, pz3 = z3;
                    double px4 = x, py4 = y + stepVal, pz4 = z4;

                    RotateAroundXAxis(ref py1, ref pz1, eyeX);
                    RotateAroundYAxis(ref px1, ref pz1, eyeY);
                    RotateAroundZAxis(ref px1, ref py1, eyeZ);

                    RotateAroundXAxis(ref py2, ref pz2, eyeX);
                    RotateAroundYAxis(ref px2, ref pz2, eyeY);
                    RotateAroundZAxis(ref px2, ref py2, eyeZ);

                    RotateAroundXAxis(ref py3, ref pz3, eyeX);
                    RotateAroundYAxis(ref px3, ref pz3, eyeY);
                    RotateAroundZAxis(ref px3, ref py3, eyeZ);

                    RotateAroundXAxis(ref py4, ref pz4, eyeX);
                    RotateAroundYAxis(ref px4, ref pz4, eyeY);
                    RotateAroundZAxis(ref px4, ref py4, eyeZ);

                    int screenX1 = (int)Math.Ceiling(((px1 - xMin) / (xMax - xMin)) * pixels);
                    int screenY1 = (int)Math.Ceiling(((py1 - yMin) / (yMax - yMin)) * pixels);
                    int screenX2 = (int)Math.Ceiling(((px2 - xMin) / (xMax - xMin)) * pixels);
                    int screenY2 = (int)Math.Ceiling(((py2 - yMin) / (yMax - yMin)) * pixels);
                    int screenX3 = (int)Math.Ceiling(((px3 - xMin) / (xMax - xMin)) * pixels);
                    int screenY3 = (int)Math.Ceiling(((py3 - yMin) / (yMax - yMin)) * pixels);
                    int screenX4 = (int)Math.Ceiling(((px4 - xMin) / (xMax - xMin)) * pixels);
                    int screenY4 = (int)Math.Ceiling(((py4 - yMin) / (yMax - yMin)) * pixels);

                    int redChannel = (int)Math.Abs(255.0 * (px1 - xMin) / (xMax - xMin));
                    int greenChannel = (int)Math.Abs(255.0 * (py1 - yMin) / (yMax - yMin));
                    int blueChannel = (int)Math.Abs(255.0 * (pz1 - zMin) / (zMax - zMin));

                    redChannel = Math.Max(0, Math.Min(255, redChannel));
                    greenChannel = Math.Max(0, Math.Min(255, greenChannel));
                    blueChannel = Math.Max(0, Math.Min(255, blueChannel));

                    int renderMode = Session["renderMode"] != null ? (int)Session["renderMode"] : 1;

                    if (renderMode == 1)
                    {
                        using (var pen = new Pen(Color.FromArgb(redChannel, greenChannel, blueChannel)))
                        {
                            graphics.DrawLine(pen, screenX1, screenY1, screenX2, screenY2);
                            graphics.DrawLine(pen, screenX1, screenY1, screenX4, screenY4);
                            graphics.DrawLine(pen, screenX2, screenY2, screenX4, screenY4);
                            graphics.DrawLine(pen, screenX2, screenY2, screenX3, screenY3);
                            graphics.DrawLine(pen, screenX3, screenY3, screenX4, screenY4);
                        }
                    }
                    else if (renderMode == 2)
                    {
                        using (var brush = new SolidBrush(Color.FromArgb(redChannel, greenChannel, blueChannel)))
                        {
                            var triangle1 = new Point[]
                            {
                                new Point(screenX1, screenY1),
                                new Point(screenX2, screenY2),
                                new Point(screenX4, screenY4)
                            };
                            graphics.FillPolygon(brush, triangle1);

                            var triangle2 = new Point[]
                            {
                                new Point(screenX2, screenY2),
                                new Point(screenX4, screenY4),
                                new Point(screenX3, screenY3)
                            };
                            graphics.FillPolygon(brush, triangle2);
                        }
                    }

                    idy++;
                    y += stepVal;
                }
                idx++;
                x += stepVal;
            }

            using (var stream = new MemoryStream())
            {
                bitmap.Save(stream, ImageFormat.Png);
                Response.ClearContent();
                Response.ContentType = "image/png";
                Response.BinaryWrite(stream.ToArray());
                Response.Flush();
            }
        }
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Surface Plotter</title></head>
<body style="text-align: center; margin: 0px; padding: 0px;">
    <form id="mainForm" runat="server">
    </form>
</body>
</html>

Main Control Page

<%@ Page Language="c#" Debug="true" Explicit="True" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="System.Globalization" %>

<script language="C#" runat="server">

    public string ReadFileContent(string filePath)
    {
        using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        using (var reader = new StreamReader(fs))
        {
            return reader.ReadToEnd();
        }
    }

    public void WriteFileContent(string filePath, string content)
    {
        using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
        }
    }

    private void Page_Load(object sender, EventArgs e)
    {
        var culture = (CultureInfo)Thread.CurrentThread.CurrentCulture.Clone();
        culture.NumberFormat.NumberDecimalSeparator = ".";
        Thread.CurrentThread.CurrentCulture = culture;

        statusLabel.Text = "";
        string formula = functionInput.Text;
        
        formula = formula.Replace("sin", "Math.Sin");
        formula = formula.Replace("cos", "Math.Cos");
        formula = formula.Replace("sqrt", "Math.Sqrt");
        formula = formula.Replace("sinh", "Math.Sinh");
        formula = formula.Replace("acos", "Math.Acos");
        formula = formula.Replace("cosh", "Math.Cosh");
        formula = formula.Replace("abs", "Math.Abs");
        formula = formula.Replace("asin", "Math.Asin");
        formula = formula.Replace("ceiling", "Math.Ceiling");
        formula = formula.Replace("exp", "Math.Exp");
        formula = formula.Replace("floor", "Math.Floor");
        formula = formula.Replace("log", "Math.Log");
        formula = formula.Replace("log10", "Math.Log10");
        formula = formula.Replace("pi", "Math.PI");
        formula = formula.Replace("pow", "Math.Pow");
        formula = formula.Replace("tan", "Math.Tan");
        formula = formula.Replace("tanh", "Math.Tanh");
        formula = formula.Replace("round", "Math.Round");


        string templateData = ReadFileContent(Server.MapPath("./templates/surface_template.txt"));
        templateData = templateData.Replace("@FORMULA", formula);
        WriteFileContent(Server.MapPath("./renders/output.aspx"), templateData);

        int displaySize = int.Parse(sizeInput.Text);

        double xLower = double.Parse(xMinInput.Text);
        double xUpper = double.Parse(xMaxInput.Text);
        double yLower = double.Parse(yMinInput.Text);
        double yUpper = double.Parse(yMaxInput.Text);
        double stepSize = double.Parse(stepInput.Text);

        if (Math.Abs(stepSize) < 1.0E-5)
        {
            Response.Write("Invalid step value");
            return;
        }

        const int maxPoints = 1000000;

        int gridCols = (int)Math.Ceiling((xUpper - xLower) / stepSize) + 1;
        int gridRows = (int)Math.Ceiling((yUpper - yLower) / stepSize) + 1;
        int pointCount = gridCols * gridRows;

        statusLabel.Text += $"Grid dimensions: {pointCount} points";
        if (pointCount > maxPoints)
        {
            Response.Write("Maximum points exceeded");
            return;
        }

        statusLabel.Text += $"<br>Grid: [{gridCols}, {gridRows}]";

        Session["pixels"] = displaySize;
        Session["xMin"] = xLower;
        Session["yMin"] = yLower;
        Session["xMax"] = xUpper;
        Session["yMax"] = yUpper;
        Session["stepVal"] = stepSize;

        Session["renderMode"] = wireframeMode.Checked ? 1 : 2;
        Session["sizeCols"] = gridCols;
        Session["sizeRows"] = gridRows;

        Session["eyeX"] = double.Parse(rotationX.Text);
        Session["eyeY"] = double.Parse(rotationY.Text);
        Session["eyeZ"] = double.Parse(rotationZ.Text);
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Interactive 3D Function Plotter</title>
</head>

<body style="text-align: center; margin: 0px; padding: 0px;">
    <form id="form1" runat="server">
        <h1>R² Surface Visualization</h1>
        <asp:Panel ID="PlotPanel" runat="server" Font-Names="Verdana" 
                   Font-Size="12px" Height="100%"
            Style="margin-left: auto; margin-right: auto" 
            Width="90%" ForeColor="DarkSlateGray" 
            ScrollBars="Vertical" BorderColor="Gray"
            BorderStyle="Solid" BorderWidth="1px">
            | \| Function \|  \| <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" height="16px" id="functionInput" runat="server" width="300px"> sin(pow(x,3)+x\*y-pow(y,3)) </textbox> \| \|---\|---\|:--\| \| Domain \|  \| <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="xMinInput" runat="server" width="32px">-2</textbox> &lt;x&lt; <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="xMaxInput" runat="server" width="32px">2 </textbox> <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="yMinInput" runat="server" width="32px">-2</textbox> &lt;y&lt; <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="yMaxInput" runat="server" width="32px">2 </textbox> \| \| Step Size \|  \| <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="stepInput" runat="server" width="40px">0.05</textbox> \| \| Image Size \|  \| <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="sizeInput" runat="server" width="32px">200</textbox>px \| \| View Mode \|  \| <radiobutton checked="True" groupname="view" id="wireframeMode" runat="server" text="Wireframe"></radiobutton> <radiobutton groupname="view" id="solidMode" runat="server" text="Solid Fill"></radiobutton> \| \| Rotation (x, y, z) \|  \| <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="rotationX" runat="server" width="40px">0.0</textbox> <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="rotationY" runat="server" width="40px">0.0</textbox> <textbox font-names="Verdana" font-size="12px" forecolor="DarkGreen" id="rotationZ" runat="server" width="40px">0.0</textbox> rad \| \|  \|  \| <button backcolor="White" font-names="Arial" id="plotButton" runat="server" text="Plot"></button> \| \|  \|  \| <label font-names="Arial" font-size="X-Small" forecolor="MidnightBlue" height="28px" id="statusLabel" runat="server" width="100%"> </label> \| |
|---|
                    </td>
                    <td>
                        | <image id="functionImage" imageurl="./renders/output.aspx" runat="server"></image> |
|---|
                    </td>
                </tr>
            </table>
        </asp:Panel>
    </form>
</body>
</html>

User Interaction Features

The visualization system supports several interactive parameters:

  • Mathematical Expression: User-defined function z = f(x, y) with support for trigonometric functions (sin, cos, tan, etc.), exponential functions (exp, log, log10), and special functions (sinh, cosh, tanh, abs, sqrt)
  • Domain Boundaries: Configurable ranges for both x and y axes
  • Discretization Granularity: Adjustable step size controlling the density of the computational grid
  • Three-Axis Rotation: Rotation angles around X, Y, and Z axes specified in radians
  • Rendering Mode: Toggle between wireframe (line-based) and solid fill (polygon-based) representations

Tags: ASP.NET 3d-graphics surface-plotting mathematical-visualization c-sharp

Posted on Sat, 24 Oct 2026 16:07:30 +0000 by Leveecius