590 lines
18 KiB
C++
590 lines
18 KiB
C++
// Filename: triangleRasterizer.cxx
|
|
// Created by: drose (06Nov99)
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
//
|
|
// PANDA 3D SOFTWARE
|
|
// Copyright (c) 2001, Disney Enterprises, Inc. All rights reserved
|
|
//
|
|
// All use of this software is subject to the terms of the Panda 3d
|
|
// Software license. You should have received a copy of this license
|
|
// along with this source code; you will also find a current copy of
|
|
// the license at http://www.panda3d.org/license.txt .
|
|
//
|
|
// To contact the maintainers of this program write to
|
|
// panda3d@yahoogroups.com .
|
|
//
|
|
////////////////////////////////////////////////////////////////////
|
|
|
|
#include "triangleRasterizer.h"
|
|
#include "stitchImage.h"
|
|
|
|
// Inline function declared up here for the forward reference.
|
|
inline void TriangleRasterizer::
|
|
filter_pixel(RGBColord &rgb, double &alpha,
|
|
double s, double t,
|
|
double dsdx, double dtdx, double dsdy, double dtdy) {
|
|
filter_pixel(rgb, alpha, s, t,
|
|
max(max(dsdx, dtdx), max(dsdy, dtdy)) / 2.0);
|
|
}
|
|
|
|
|
|
TriangleRasterizer::Edge::
|
|
Edge(const RasterizerVertex *v0, const RasterizerVertex *v1) :
|
|
_v0(v0), _v1(v1)
|
|
{
|
|
_dx = v1->_p[0] - v0->_p[0];
|
|
_dy = v1->_p[1] - v0->_p[1];
|
|
}
|
|
|
|
TriangleRasterizer::
|
|
TriangleRasterizer() {
|
|
_output = NULL;
|
|
_input = NULL;
|
|
_read_input = false;
|
|
_texture = NULL;
|
|
_filter_output = false;
|
|
_untextured_color.set(1.0, 1.0, 1.0, 1.0);
|
|
}
|
|
|
|
void TriangleRasterizer::
|
|
draw_triangle(const RasterizerVertex *v0,
|
|
const RasterizerVertex *v1,
|
|
const RasterizerVertex *v2) {
|
|
if ((v0->_visibility & v1->_visibility & v2->_visibility) != 0) {
|
|
// All three vertices are out of bounds in the same direction, so
|
|
// the triangle is completely out of bounds. Don't bother trying
|
|
// to draw it.
|
|
return;
|
|
}
|
|
if (v0->_visibility < 0 || v1->_visibility < 0 || v2->_visibility < 0) {
|
|
// At least one vertex is totally bogus, so throw up our hands on
|
|
// the triangle.
|
|
return;
|
|
}
|
|
|
|
assert(_output != NULL);
|
|
if (!_read_input) {
|
|
read_input();
|
|
}
|
|
|
|
double oneOverArea;
|
|
const RasterizerVertex *vMin, *vMid, *vMax;
|
|
/* Y(vMin)<=Y(vMid)<=Y(vMax) */
|
|
|
|
/* find the order of the 3 vertices along the Y axis */
|
|
{
|
|
double y0 = v0->_p[1];
|
|
double y1 = v1->_p[1];
|
|
double y2 = v2->_p[1];
|
|
|
|
if (y0<=y1) {
|
|
if (y1<=y2) {
|
|
vMin = v0; vMid = v1; vMax = v2; /* y0<=y1<=y2 */
|
|
} else if (y2<=y0) {
|
|
vMin = v2; vMid = v0; vMax = v1; /* y2<=y0<=y1 */
|
|
} else {
|
|
vMin = v0; vMid = v2; vMax = v1; /* y0<=y2<=y1 */
|
|
}
|
|
} else {
|
|
if (y0<=y2) {
|
|
vMin = v1; vMid = v0; vMax = v2; /* y1<=y0<=y2 */
|
|
} else if (y2<=y1) {
|
|
vMin = v2; vMid = v1; vMax = v0; /* y2<=y1<=y0 */
|
|
} else {
|
|
vMin = v1; vMid = v2; vMax = v0; /* y1<=y2<=y0 */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* vertex/edge relationship */
|
|
Edge eMaj(vMin, vMax);
|
|
Edge eTop(vMid, vMax);
|
|
Edge eBot(vMin, vMid);
|
|
|
|
/* compute oneOverArea */
|
|
{
|
|
double area = eMaj._dx * eBot._dy - eBot._dx * eMaj._dy;
|
|
|
|
// We can't cull very small triangles; we might generate small
|
|
// triangles through normal operations.
|
|
|
|
/*
|
|
if (area>-0.05 && area<0.05) {
|
|
return; // very small; CULLED
|
|
}
|
|
*/
|
|
oneOverArea = 1.0 / area;
|
|
}
|
|
|
|
/* Edge setup. For a triangle strip these could be reused... */
|
|
{
|
|
/* fixed point Y coordinates */
|
|
FixedPoint vMin_fx = FloatToFixed(vMin->_p[0] + 0.5);
|
|
FixedPoint vMin_fy = FloatToFixed(vMin->_p[1] - 0.5);
|
|
FixedPoint vMid_fx = FloatToFixed(vMid->_p[0] + 0.5);
|
|
FixedPoint vMid_fy = FloatToFixed(vMid->_p[1] - 0.5);
|
|
FixedPoint vMax_fy = FloatToFixed(vMax->_p[1] - 0.5);
|
|
|
|
eMaj._fsy = FixedCeil(vMin_fy);
|
|
eMaj._lines = FixedToInt(vMax_fy + FIXED_ONE - FIXED_EPSILON - eMaj._fsy);
|
|
if (eMaj._lines > 0) {
|
|
double dxdy = eMaj._dx / eMaj._dy;
|
|
eMaj._fdxdy = SignedFloatToFixed(dxdy);
|
|
eMaj._adjy = (double) (eMaj._fsy - vMin_fy); /* SCALED! */
|
|
eMaj._fx0 = vMin_fx;
|
|
eMaj._fsx = eMaj._fx0 + (FixedPoint) (eMaj._adjy * dxdy);
|
|
}
|
|
else {
|
|
return; /*CULLED*/
|
|
}
|
|
|
|
eTop._fsy = FixedCeil(vMid_fy);
|
|
eTop._lines = FixedToInt(vMax_fy + FIXED_ONE - FIXED_EPSILON - eTop._fsy);
|
|
if (eTop._lines > 0) {
|
|
double dxdy = eTop._dx / eTop._dy;
|
|
eTop._fdxdy = SignedFloatToFixed(dxdy);
|
|
eTop._adjy = (double) (eTop._fsy - vMid_fy); /* SCALED! */
|
|
eTop._fx0 = vMid_fx;
|
|
eTop._fsx = eTop._fx0 + (FixedPoint) (eTop._adjy * dxdy);
|
|
}
|
|
|
|
eBot._fsy = FixedCeil(vMin_fy);
|
|
eBot._lines = FixedToInt(vMid_fy + FIXED_ONE - FIXED_EPSILON - eBot._fsy);
|
|
if (eBot._lines > 0) {
|
|
double dxdy = eBot._dx / eBot._dy;
|
|
eBot._fdxdy = SignedFloatToFixed(dxdy);
|
|
eBot._adjy = (double) (eBot._fsy - vMin_fy); /* SCALED! */
|
|
eBot._fx0 = vMin_fx;
|
|
eBot._fsx = eBot._fx0 + (FixedPoint) (eBot._adjy * dxdy);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Conceptually, we view a triangle as two subtriangles
|
|
* separated by a perfectly horizontal line. The edge that is
|
|
* intersected by this line is one with maximal absolute dy; we
|
|
* call it a ``major'' edge. The other two edges are the
|
|
* ``top'' edge (for the upper subtriangle) and the ``bottom''
|
|
* edge (for the lower subtriangle). If either of these two
|
|
* edges is horizontal or very close to horizontal, the
|
|
* corresponding subtriangle might cover zero sample points;
|
|
* we take care to handle such cases, for performance as well
|
|
* as correctness.
|
|
*
|
|
* By stepping rasterization parameters along the major edge,
|
|
* we can avoid recomputing them at the discontinuity where
|
|
* the top and bottom edges meet. However, this forces us to
|
|
* be able to scan both left-to-right and right-to-left.
|
|
* Also, we must determine whether the major edge is at the
|
|
* left or right side of the triangle. We do this by
|
|
* computing the magnitude of the cross-product of the major
|
|
* and top edges. Since this magnitude depends on the sine of
|
|
* the angle between the two edges, its sign tells us whether
|
|
* we turn to the left or to the right when travelling along
|
|
* the major edge to the top edge, and from this we infer
|
|
* whether the major edge is on the left or the right.
|
|
*
|
|
* Serendipitously, this cross-product magnitude is also a
|
|
* value we need to compute the iteration parameter
|
|
* derivatives for the triangle, and it can be used to perform
|
|
* backface culling because its sign tells us whether the
|
|
* triangle is clockwise or counterclockwise. In this code we
|
|
* refer to it as ``area'' because it's also proportional to
|
|
* the pixel area of the triangle.
|
|
*/
|
|
|
|
{
|
|
int ltor; /* true if scanning left-to-right */
|
|
|
|
// For interpolating the alpha value.
|
|
double dadx, dady;
|
|
FixedPoint fdadx;
|
|
|
|
// For interpolating texture coordinates.
|
|
double dsdx, dsdy;
|
|
FixedPoint fdsdx;
|
|
double dtdx, dtdy;
|
|
FixedPoint fdtdx;
|
|
|
|
// Set up values for texture coordinates.
|
|
|
|
double twidth, theight;
|
|
if (_texture != NULL) {
|
|
twidth = (double) _texture->get_x_size();
|
|
theight = (double) _texture->get_y_size();
|
|
} else {
|
|
twidth = 1.0;
|
|
theight = 1.0;
|
|
}
|
|
|
|
ltor = (oneOverArea < 0.0);
|
|
|
|
// More alpha setup.
|
|
{
|
|
double eMaj_da, eBot_da;
|
|
eMaj_da = vMax->_alpha - vMin->_alpha;
|
|
eBot_da = vMid->_alpha - vMin->_alpha;
|
|
dadx = oneOverArea * (eMaj_da * eBot._dy - eMaj._dy * eBot_da);
|
|
fdadx = SignedFloatToFixed(dadx);
|
|
dady = oneOverArea * (eMaj._dx * eBot_da - eMaj_da * eBot._dx);
|
|
}
|
|
|
|
// Texture coordinates.
|
|
{
|
|
double eMaj_ds, eBot_ds;
|
|
eMaj_ds = (vMax->_uv[0] - vMin->_uv[0]) * twidth;
|
|
eBot_ds = (vMid->_uv[0] - vMin->_uv[0]) * twidth;
|
|
|
|
dsdx = oneOverArea * (eMaj_ds * eBot._dy - eMaj._dy * eBot_ds);
|
|
fdsdx = SignedFloatToFixed(dsdx);
|
|
dsdy = oneOverArea * (eMaj._dx * eBot_ds - eMaj_ds * eBot._dx);
|
|
}
|
|
{
|
|
double eMaj_dt, eBot_dt;
|
|
eMaj_dt = (vMax->_uv[1] - vMin->_uv[1]) * theight;
|
|
eBot_dt = (vMid->_uv[1] - vMin->_uv[1]) * theight;
|
|
|
|
dtdx = oneOverArea * (eMaj_dt * eBot._dy - eMaj._dy * eBot_dt);
|
|
fdtdx = SignedFloatToFixed(dtdx);
|
|
dtdy = oneOverArea * (eMaj._dx * eBot_dt - eMaj_dt * eBot._dx);
|
|
}
|
|
|
|
/*
|
|
* We always sample at pixel centers. However, we avoid
|
|
* explicit half-pixel offsets in this code by incorporating
|
|
* the proper offset in each of x and y during the
|
|
* transformation to window coordinates.
|
|
*
|
|
* We also apply the usual rasterization rules to prevent
|
|
* cracks and overlaps. A pixel is considered inside a
|
|
* subtriangle if it meets all of four conditions: it is on or
|
|
* to the right of the left edge, strictly to the left of the
|
|
* right edge, on or below the top edge, and strictly above
|
|
* the bottom edge. (Some edges may be degenerate.)
|
|
*
|
|
* The following discussion assumes left-to-right scanning
|
|
* (that is, the major edge is on the left); the right-to-left
|
|
* case is a straightforward variation.
|
|
*
|
|
* We start by finding the half-integral y coordinate that is
|
|
* at or below the top of the triangle. This gives us the
|
|
* first scan line that could possibly contain pixels that are
|
|
* inside the triangle.
|
|
*
|
|
* Next we creep down the major edge until we reach that y,
|
|
* and compute the corresponding x coordinate on the edge.
|
|
* Then we find the half-integral x that lies on or just
|
|
* inside the edge. This is the first pixel that might lie in
|
|
* the interior of the triangle. (We won't know for sure
|
|
* until we check the other edges.)
|
|
*
|
|
* As we rasterize the triangle, we'll step down the major
|
|
* edge. For each step in y, we'll move an integer number
|
|
* of steps in x. There are two possible x step sizes, which
|
|
* we'll call the ``inner'' step (guaranteed to land on the
|
|
* edge or inside it) and the ``outer'' step (guaranteed to
|
|
* land on the edge or outside it). The inner and outer steps
|
|
* differ by one. During rasterization we maintain an error
|
|
* term that indicates our distance from the true edge, and
|
|
* select either the inner step or the outer step, whichever
|
|
* gets us to the first pixel that falls inside the triangle.
|
|
*
|
|
* All parameters (z, red, etc.) as well as the buffer
|
|
* addresses for color and z have inner and outer step values,
|
|
* so that we can increment them appropriately. This method
|
|
* eliminates the need to adjust parameters by creeping a
|
|
* sub-pixel amount into the triangle at each scanline.
|
|
*/
|
|
|
|
{
|
|
int subTriangle;
|
|
FixedPoint fx, fxLeftEdge, fxRightEdge, fdxLeftEdge, fdxRightEdge;
|
|
FixedPoint fdxOuter;
|
|
int idxOuter;
|
|
double dxOuter;
|
|
FixedPoint fError, fdError;
|
|
double adjx, adjy;
|
|
FixedPoint fy;
|
|
int iy;
|
|
|
|
// Alpha.
|
|
FixedPoint fa, fdaOuter, fdaInner;
|
|
|
|
// Texture coordinates.
|
|
FixedPoint fs, fdsOuter, fdsInner;
|
|
FixedPoint ft, fdtOuter, fdtInner;
|
|
|
|
for (subTriangle=0; subTriangle<=1; subTriangle++) {
|
|
Edge *eLeft, *eRight;
|
|
int setupLeft, setupRight;
|
|
int lines;
|
|
|
|
if (subTriangle==0) {
|
|
/* bottom half */
|
|
if (ltor) {
|
|
eLeft = &eMaj;
|
|
eRight = &eBot;
|
|
lines = eRight->_lines;
|
|
setupLeft = 1;
|
|
setupRight = 1;
|
|
}
|
|
else {
|
|
eLeft = &eBot;
|
|
eRight = &eMaj;
|
|
lines = eLeft->_lines;
|
|
setupLeft = 1;
|
|
setupRight = 1;
|
|
}
|
|
}
|
|
else {
|
|
/* top half */
|
|
if (ltor) {
|
|
eLeft = &eMaj;
|
|
eRight = &eTop;
|
|
lines = eRight->_lines;
|
|
setupLeft = 0;
|
|
setupRight = 1;
|
|
}
|
|
else {
|
|
eLeft = &eTop;
|
|
eRight = &eMaj;
|
|
lines = eLeft->_lines;
|
|
setupLeft = 1;
|
|
setupRight = 0;
|
|
}
|
|
if (lines==0) return;
|
|
}
|
|
|
|
if (setupLeft && eLeft->_lines>0) {
|
|
const RasterizerVertex *vLower;
|
|
FixedPoint fsx = eLeft->_fsx;
|
|
fx = FixedCeil(fsx);
|
|
fError = fx - fsx - FIXED_ONE;
|
|
fxLeftEdge = fsx - FIXED_EPSILON;
|
|
fdxLeftEdge = eLeft->_fdxdy;
|
|
fdxOuter = FixedFloor(fdxLeftEdge - FIXED_EPSILON);
|
|
fdError = fdxOuter - fdxLeftEdge + FIXED_ONE;
|
|
idxOuter = FixedToInt(fdxOuter);
|
|
dxOuter = (double) idxOuter;
|
|
|
|
fy = eLeft->_fsy;
|
|
iy = FixedToInt(fy);
|
|
|
|
adjx = (double)(fx - eLeft->_fx0); /* SCALED! */
|
|
adjy = eLeft->_adjy; /* SCALED! */
|
|
|
|
vLower = eLeft->_v0;
|
|
|
|
/*
|
|
* Now we need the set of parameter (z, color, etc.) values at
|
|
* the point (fx, fy). This gives us properly-sampled parameter
|
|
* values that we can step from pixel to pixel. Furthermore,
|
|
* although we might have intermediate results that overflow
|
|
* the normal parameter range when we step temporarily outside
|
|
* the triangle, we shouldn't overflow or underflow for any
|
|
* pixel that's actually inside the triangle.
|
|
*/
|
|
|
|
// Interpolate alpha
|
|
fa = (FixedPoint)(vLower->_alpha * FIXED_SCALE + dadx * adjx + dady * adjy)
|
|
+ FIXED_HALF;
|
|
fdaOuter = SignedFloatToFixed(dady + dxOuter * dadx);
|
|
// Interpolate texture coordinates
|
|
{
|
|
double s0, t0;
|
|
s0 = vLower->_uv[0] * twidth;
|
|
fs = (FixedPoint)(s0 * FIXED_SCALE + dsdx * adjx + dsdy * adjy) + FIXED_HALF;
|
|
fdsOuter = SignedFloatToFixed(dsdy + dxOuter * dsdx);
|
|
t0 = vLower->_uv[1] * theight;
|
|
ft = (FixedPoint)(t0 * FIXED_SCALE + dtdx * adjx + dtdy * adjy) + FIXED_HALF;
|
|
fdtOuter = SignedFloatToFixed(dtdy + dxOuter * dtdx);
|
|
}
|
|
|
|
} /*if setupLeft*/
|
|
|
|
|
|
if (setupRight && eRight->_lines>0) {
|
|
fxRightEdge = eRight->_fsx - FIXED_EPSILON;
|
|
fdxRightEdge = eRight->_fdxdy;
|
|
}
|
|
|
|
if (lines==0) {
|
|
continue;
|
|
}
|
|
|
|
/* Rasterize setup */
|
|
fdaInner = fdaOuter + fdadx;
|
|
fdsInner = fdsOuter + fdsdx;
|
|
fdtInner = fdtOuter + fdtdx;
|
|
|
|
while (lines>0) {
|
|
if (iy >= 0 && iy < _output->get_y_size()) {
|
|
/* initialize the span interpolants to the leftmost value */
|
|
/* ff = fixed-pt fragment */
|
|
FixedPoint ffa = fa;
|
|
FixedPoint ffs = fs, fft = ft;
|
|
|
|
int left = FixedToInt(fxLeftEdge);
|
|
int right = FixedToInt(fxRightEdge);
|
|
|
|
// Alpha
|
|
{
|
|
// FixedPoint ffaend = ffa+(right-left-1)*fdadx;
|
|
// if (ffaend<0) ffa -= ffaend;
|
|
// if (ffa<0) ffa = 0;
|
|
}
|
|
|
|
// Rasterize left to right at row iy.
|
|
if (right > left) {
|
|
ffs -= FIXED_HALF; /* off-by-one error? */
|
|
fft -= FIXED_HALF;
|
|
ffa -= FIXED_HALF;
|
|
for (int ix = left; ix < right; ix++) {
|
|
if (ix >= 0 && ix < _output->get_x_size()) {
|
|
RGBColord rgb;
|
|
double alpha;
|
|
filter_pixel(rgb, alpha,
|
|
FixedToFloat(ffs), FixedToFloat(fft),
|
|
dsdx, dtdx, dsdy, dtdy);
|
|
alpha *= FixedToFloat(ffa);
|
|
_output->blend(ix, iy, rgb, alpha);
|
|
}
|
|
|
|
ffs += fdsdx;
|
|
fft += fdtdx;
|
|
ffa += fdadx;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Advance to the next scan line. Compute the
|
|
* new edge coordinates, and adjust the
|
|
* pixel-center x coordinate so that it stays
|
|
* on or inside the major edge.
|
|
*/
|
|
iy++;
|
|
lines--;
|
|
|
|
fxLeftEdge += fdxLeftEdge;
|
|
fxRightEdge += fdxRightEdge;
|
|
|
|
fError += fdError;
|
|
if (fError >= 0) {
|
|
fError -= FIXED_ONE;
|
|
|
|
fa += fdaOuter;
|
|
fs += fdsOuter;
|
|
ft += fdtOuter;
|
|
} else {
|
|
fa += fdaInner;
|
|
fs += fdsInner;
|
|
ft += fdtInner;
|
|
}
|
|
} /*while lines>0*/
|
|
|
|
} /* for subTriangle */
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
void TriangleRasterizer::
|
|
draw_pixel(const RasterizerVertex *v0, double radius) {
|
|
if (v0->_visibility != 0) {
|
|
// The pixel is off the screen.
|
|
return;
|
|
}
|
|
int ix = (int)v0->_p[0];
|
|
int iy = (int)v0->_p[1];
|
|
|
|
if (iy >= 0 && iy < _output->get_y_size() &&
|
|
ix >= 0 && ix < _output->get_x_size()) {
|
|
if (!_read_input) {
|
|
read_input();
|
|
}
|
|
|
|
RGBColord rgb;
|
|
double alpha;
|
|
if (_texture == NULL) {
|
|
filter_pixel(rgb, alpha, v0->_uv[0], v0->_uv[1], radius);
|
|
} else {
|
|
filter_pixel(rgb, alpha,
|
|
v0->_uv[0] * (_texture->get_x_size() - 1),
|
|
v0->_uv[1] * (_texture->get_y_size() - 1),
|
|
radius * (_texture->get_x_size() - 1));
|
|
}
|
|
alpha *= v0->_alpha;
|
|
_output->blend(ix, iy, rgb, alpha);
|
|
}
|
|
}
|
|
|
|
void TriangleRasterizer::
|
|
filter_pixel(RGBColord &rgb, double &alpha,
|
|
double s, double t, double radius) {
|
|
if (_texture == NULL) {
|
|
rgb.set(_untextured_color[0],
|
|
_untextured_color[1],
|
|
_untextured_color[2]);
|
|
alpha = _untextured_color[3];
|
|
return;
|
|
}
|
|
|
|
int ri = (int)radius;
|
|
int si = (int)(s + 0.5);
|
|
int ti = _texture->get_y_size() - 1 - (int)(t + 0.5);
|
|
|
|
rgb.set(0.0, 0.0, 0.0);
|
|
alpha = 0.0;
|
|
|
|
if (!_filter_output) {
|
|
if (si >= 0 && si < _texture->get_x_size() &&
|
|
ti >= 0 && ti < _texture->get_y_size()) {
|
|
rgb = _texture->get_xel(si, ti);
|
|
alpha = 1.0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
int num_total = 0;
|
|
int num_visible = 0;
|
|
for (int yr = -ri; yr <= ri; yr++) {
|
|
int tii = ti + yr;
|
|
for (int xr = -ri; xr <= ri; xr++) {
|
|
int sii = si + xr;
|
|
if (sii >= 0 && sii < _texture->get_x_size() &&
|
|
tii >= 0 && tii < _texture->get_y_size()) {
|
|
rgb += _texture->get_xel(sii, tii);
|
|
num_visible++;
|
|
}
|
|
num_total++;
|
|
}
|
|
}
|
|
|
|
if (num_visible != 0) {
|
|
rgb /= (double)num_visible;
|
|
alpha = 1.0;
|
|
}
|
|
|
|
// We would do this to antialias the edge of the image. However, it
|
|
// seems to cause problems at seams, so we won't do it.
|
|
/*
|
|
if (num_total != 0) {
|
|
alpha = (double)num_visible / (double)num_total;
|
|
}
|
|
*/
|
|
}
|
|
|
|
void TriangleRasterizer::
|
|
read_input() {
|
|
if (_input != NULL) {
|
|
if (!_input->read_file()) {
|
|
nout << "Unable to read image.\n";
|
|
} else {
|
|
_texture = _input->_data;
|
|
}
|
|
}
|
|
_read_input = true;
|
|
}
|