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:
- Read the template file containing the function placeholder
- Replace the placeholder with the user-supplied expression
- 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> <x< <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> <y< <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