dielectric now puts equal weight on both components, locale fixes, many importer improvements
parent
fa1789be6d
commit
f0a2e2436e
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
MTS_NAMESPACE_BEGIN
|
MTS_NAMESPACE_BEGIN
|
||||||
|
|
||||||
|
const bool importanceSampleComponents = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models an interface between two materials with non-matched indices of refraction.
|
* Models an interface between two materials with non-matched indices of refraction.
|
||||||
* The microscopic surface structure is assumed to be perfectly flat, resulting
|
* The microscopic surface structure is assumed to be perfectly flat, resulting
|
||||||
|
@ -169,15 +171,15 @@ public:
|
||||||
/* Calculate the refracted/reflected vectors+coefficients */
|
/* Calculate the refracted/reflected vectors+coefficients */
|
||||||
if (sampleTransmission && sampleReflection) {
|
if (sampleTransmission && sampleReflection) {
|
||||||
/* Importance sample according to the reflectance/transmittance */
|
/* Importance sample according to the reflectance/transmittance */
|
||||||
if (bRec.sample.x < fr) {
|
if (bRec.sample.x < importanceSampleComponents ? fr : 0.5f) {
|
||||||
reflect(bRec.wi, bRec.wo);
|
reflect(bRec.wi, bRec.wo);
|
||||||
bRec.sampledComponent = 0;
|
bRec.sampledComponent = 0;
|
||||||
bRec.sampledType = EDeltaReflection;
|
bRec.sampledType = EDeltaReflection;
|
||||||
pdf = fr * std::abs(Frame::cosTheta(bRec.wo));
|
pdf = (importanceSampleComponents ? fr : 0.5f) * std::abs(Frame::cosTheta(bRec.wo));
|
||||||
/* Cancel out the cosine term */
|
/* Cancel out the cosine term */
|
||||||
return m_reflectance * fr;
|
return m_reflectance * fr;
|
||||||
} else {
|
} else {
|
||||||
pdf = 1-fr;
|
pdf = importanceSampleComponents ? (1-fr) : 0.5f;
|
||||||
bRec.sampledComponent = 1;
|
bRec.sampledComponent = 1;
|
||||||
bRec.sampledType = EDeltaTransmission;
|
bRec.sampledType = EDeltaTransmission;
|
||||||
|
|
||||||
|
@ -217,11 +219,15 @@ public:
|
||||||
|
|
||||||
Float result = 0.0f;
|
Float result = 0.0f;
|
||||||
if (sampleTransmission && sampleReflection) {
|
if (sampleTransmission && sampleReflection) {
|
||||||
Float fr = fresnel(Frame::cosTheta(bRec.wi), m_extIOR, m_intIOR);
|
if (!importanceSampleComponents) {
|
||||||
if (reflection)
|
result = 0.f;
|
||||||
result = fr;
|
} else {
|
||||||
else
|
Float fr = fresnel(Frame::cosTheta(bRec.wi), m_extIOR, m_intIOR);
|
||||||
result = 1-fr;
|
if (reflection)
|
||||||
|
result = fr;
|
||||||
|
else
|
||||||
|
result = 1-fr;
|
||||||
|
}
|
||||||
} else if (sampleReflection) {
|
} else if (sampleReflection) {
|
||||||
result = reflection ? 1.0f : 0.0f;
|
result = reflection ? 1.0f : 0.0f;
|
||||||
} else if (sampleTransmission) {
|
} else if (sampleTransmission) {
|
||||||
|
|
|
@ -369,7 +369,18 @@ void writeGeometry(std::string id, int geomIndex, std::string matID, Transform t
|
||||||
|
|
||||||
void loadGeometry(std::string nodeName, Transform transform, std::ostream &os, domGeometry &geom,
|
void loadGeometry(std::string nodeName, Transform transform, std::ostream &os, domGeometry &geom,
|
||||||
StringMap &matLookupTable) {
|
StringMap &matLookupTable) {
|
||||||
SLog(EInfo, "Converting geometry \"%s\" (instantiated by %s)..", geom.getName(), nodeName.c_str());
|
std::string geomName;
|
||||||
|
if (geom.getName() != NULL) {
|
||||||
|
geomName = geom.getName();
|
||||||
|
} else {
|
||||||
|
if (geom.getId() != NULL) {
|
||||||
|
geomName = geom.getId();
|
||||||
|
} else {
|
||||||
|
static int unnamedGeomCtr = 0;
|
||||||
|
geomName = formatString("unnamedGeom_%i", unnamedGeomCtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SLog(EInfo, "Converting geometry \"%s\" (instantiated by %s)..", geomName.c_str(), nodeName.c_str());
|
||||||
|
|
||||||
domMesh *mesh = geom.getMesh().cast();
|
domMesh *mesh = geom.getMesh().cast();
|
||||||
if (!mesh)
|
if (!mesh)
|
||||||
|
@ -718,7 +729,7 @@ void loadImage(ColladaConverter *cvt, std::ostream &os, domImage &image, StringM
|
||||||
os << "\t</texture>" << endl << endl;
|
os << "\t</texture>" << endl << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadCamera(Transform transform, std::ostream &os, domCamera &camera) {
|
void loadCamera(ColladaConverter *cvt, Transform transform, std::ostream &os, domCamera &camera) {
|
||||||
SLog(EInfo, "Converting camera \"%s\" ..", camera.getName());
|
SLog(EInfo, "Converting camera \"%s\" ..", camera.getName());
|
||||||
Float aspect = 1.0f;
|
Float aspect = 1.0f;
|
||||||
int xres=768;
|
int xres=768;
|
||||||
|
@ -737,6 +748,10 @@ void loadCamera(Transform transform, std::ostream &os, domCamera &camera) {
|
||||||
if (ortho) {
|
if (ortho) {
|
||||||
if (ortho->getAspect_ratio().cast() != 0)
|
if (ortho->getAspect_ratio().cast() != 0)
|
||||||
aspect = (Float) ortho->getAspect_ratio()->getValue();
|
aspect = (Float) ortho->getAspect_ratio()->getValue();
|
||||||
|
if (cvt->m_xres != -1) {
|
||||||
|
xres = cvt->m_xres;
|
||||||
|
aspect = (Float) cvt->m_xres / (Float) cvt->m_yres;
|
||||||
|
}
|
||||||
os << "\t<camera id=\"" << camera.getId() << "\" type=\"orthographic\">" << endl;
|
os << "\t<camera id=\"" << camera.getId() << "\" type=\"orthographic\">" << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -745,6 +760,10 @@ void loadCamera(Transform transform, std::ostream &os, domCamera &camera) {
|
||||||
if (persp) {
|
if (persp) {
|
||||||
if (persp->getAspect_ratio().cast() != 0)
|
if (persp->getAspect_ratio().cast() != 0)
|
||||||
aspect = (Float) persp->getAspect_ratio()->getValue();
|
aspect = (Float) persp->getAspect_ratio()->getValue();
|
||||||
|
if (cvt->m_xres != -1) {
|
||||||
|
xres = cvt->m_xres;
|
||||||
|
aspect = (Float) cvt->m_xres / (Float) cvt->m_yres;
|
||||||
|
}
|
||||||
os << "\t<camera id=\"" << camera.getId() << "\" type=\"perspective\">" << endl;
|
os << "\t<camera id=\"" << camera.getId() << "\" type=\"perspective\">" << endl;
|
||||||
if (persp->getXfov().cast()) {
|
if (persp->getXfov().cast()) {
|
||||||
Float yFov = radToDeg(2 * std::atan(std::tan(degToRad((Float) persp->getXfov()->getValue())/2) / aspect));
|
Float yFov = radToDeg(2 * std::atan(std::tan(degToRad((Float) persp->getXfov()->getValue())/2) / aspect));
|
||||||
|
@ -754,6 +773,7 @@ void loadCamera(Transform transform, std::ostream &os, domCamera &camera) {
|
||||||
os << "\t\t<float name=\"fov\" value=\"" << yFov << "\"/>" << endl;
|
os << "\t\t<float name=\"fov\" value=\"" << yFov << "\"/>" << endl;
|
||||||
os << "\t\t<float name=\"nearClip\" value=\"" << persp->getZnear()->getValue() << "\"/>" << endl;
|
os << "\t\t<float name=\"nearClip\" value=\"" << persp->getZnear()->getValue() << "\"/>" << endl;
|
||||||
os << "\t\t<float name=\"farClip\" value=\"" << persp->getZfar()->getValue() << "\"/>" << endl;
|
os << "\t\t<float name=\"farClip\" value=\"" << persp->getZfar()->getValue() << "\"/>" << endl;
|
||||||
|
os << "\t\t<boolean name=\"mapSmallerSide\" value=\"" <<*(cvt->m_mapSmallerSide ? "true" : "false") << "\"/>" << endl;
|
||||||
} else if (persp->getYfov().cast()) {
|
} else if (persp->getYfov().cast()) {
|
||||||
Float xFov = radToDeg(2 * std::atan(std::tan(degToRad((Float) persp->getYfov()->getValue())/2) * aspect));
|
Float xFov = radToDeg(2 * std::atan(std::tan(degToRad((Float) persp->getYfov()->getValue())/2) * aspect));
|
||||||
if (aspect > 1.0f)
|
if (aspect > 1.0f)
|
||||||
|
@ -762,6 +782,7 @@ void loadCamera(Transform transform, std::ostream &os, domCamera &camera) {
|
||||||
os << "\t\t<float name=\"fov\" value=\"" << xFov << "\"/>" << endl;
|
os << "\t\t<float name=\"fov\" value=\"" << xFov << "\"/>" << endl;
|
||||||
os << "\t\t<float name=\"nearClip\" value=\"" << persp->getZnear()->getValue() << "\"/>" << endl;
|
os << "\t\t<float name=\"nearClip\" value=\"" << persp->getZnear()->getValue() << "\"/>" << endl;
|
||||||
os << "\t\t<float name=\"farClip\" value=\"" << persp->getZfar()->getValue() << "\"/>" << endl;
|
os << "\t\t<float name=\"farClip\" value=\"" << persp->getZfar()->getValue() << "\"/>" << endl;
|
||||||
|
os << "\t\t<boolean name=\"mapSmallerSide\" value=\"" << (cvt->m_mapSmallerSide ? "true" : "false") << "\"/>" << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -780,8 +801,19 @@ void loadCamera(Transform transform, std::ostream &os, domCamera &camera) {
|
||||||
os << "\t</camera>" << endl << endl;
|
os << "\t</camera>" << endl << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadNode(Transform transform, std::ostream &os, domNode &node) {
|
void loadNode(ColladaConverter *cvt, Transform transform, std::ostream &os, domNode &node) {
|
||||||
SLog(EInfo, "Converting node \"%s\" ..", node.getName());
|
std::string nodeName;
|
||||||
|
if (node.getName() != NULL) {
|
||||||
|
nodeName = node.getName();
|
||||||
|
} else {
|
||||||
|
if (node.getId() != NULL) {
|
||||||
|
nodeName = node.getId();
|
||||||
|
} else {
|
||||||
|
static int unnamedNodeCtr = 0;
|
||||||
|
nodeName = formatString("unnamedNode_%i", unnamedNodeCtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SLog(EInfo, "Converting node \"%s\" ..", nodeName.c_str());
|
||||||
|
|
||||||
daeTArray<daeSmartRef<daeElement> > children = node.getChildren();
|
daeTArray<daeSmartRef<daeElement> > children = node.getChildren();
|
||||||
/* Parse transformations */
|
/* Parse transformations */
|
||||||
|
@ -840,7 +872,7 @@ void loadNode(Transform transform, std::ostream &os, domNode &node) {
|
||||||
|
|
||||||
if (!geom)
|
if (!geom)
|
||||||
SLog(EError, "Could not find a referenced geometry object!");
|
SLog(EError, "Could not find a referenced geometry object!");
|
||||||
loadGeometry(node.getName(), transform, os, *geom, matLookupTable);
|
loadGeometry(nodeName, transform, os, *geom, matLookupTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Iterate over all light references */
|
/* Iterate over all light references */
|
||||||
|
@ -860,13 +892,13 @@ void loadNode(Transform transform, std::ostream &os, domNode &node) {
|
||||||
domCamera *camera = daeSafeCast<domCamera>(inst->getUrl().getElement());
|
domCamera *camera = daeSafeCast<domCamera>(inst->getUrl().getElement());
|
||||||
if (camera == NULL)
|
if (camera == NULL)
|
||||||
SLog(EError, "Could not find a referenced camera!");
|
SLog(EError, "Could not find a referenced camera!");
|
||||||
loadCamera(transform, os, *camera);
|
loadCamera(cvt, transform, os, *camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Recursively iterate through sub-nodes */
|
/* Recursively iterate through sub-nodes */
|
||||||
domNode_Array &nodes = node.getNode_array();
|
domNode_Array &nodes = node.getNode_array();
|
||||||
for (size_t i=0; i<nodes.getCount(); ++i)
|
for (size_t i=0; i<nodes.getCount(); ++i)
|
||||||
loadNode(transform, os, *nodes[i]);
|
loadNode(cvt, transform, os, *nodes[i]);
|
||||||
|
|
||||||
/* Recursively iterate through <instance_node> elements */
|
/* Recursively iterate through <instance_node> elements */
|
||||||
domInstance_node_Array &instanceNodes = node.getInstance_node_array();
|
domInstance_node_Array &instanceNodes = node.getInstance_node_array();
|
||||||
|
@ -874,7 +906,7 @@ void loadNode(Transform transform, std::ostream &os, domNode &node) {
|
||||||
domNode *node = daeSafeCast<domNode>(instanceNodes[i]->getUrl().getElement());
|
domNode *node = daeSafeCast<domNode>(instanceNodes[i]->getUrl().getElement());
|
||||||
if (!node)
|
if (!node)
|
||||||
SLog(EError, "Could not find a referenced node!");
|
SLog(EError, "Could not find a referenced node!");
|
||||||
loadNode(transform, os, *node);
|
loadNode(cvt, transform, os, *node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -980,7 +1012,7 @@ void ColladaConverter::convert(const std::string &inputFile,
|
||||||
os << "\tAutomatically converted from COLLADA" << endl << endl;
|
os << "\tAutomatically converted from COLLADA" << endl << endl;
|
||||||
os << "-->" << endl << endl;
|
os << "-->" << endl << endl;
|
||||||
os << "<scene>" << endl;
|
os << "<scene>" << endl;
|
||||||
os << "\t<integrator type=\"direct\"/>" << endl << endl;
|
os << "\t<integrator id=\"integrator\" type=\"direct\"/>" << endl << endl;
|
||||||
|
|
||||||
SLog(EInfo, "Converting to \"%s\" ..", outputFile.c_str());
|
SLog(EInfo, "Converting to \"%s\" ..", outputFile.c_str());
|
||||||
|
|
||||||
|
@ -1000,7 +1032,7 @@ void ColladaConverter::convert(const std::string &inputFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i=0; i<nodes.getCount(); ++i)
|
for (size_t i=0; i<nodes.getCount(); ++i)
|
||||||
loadNode(Transform(), os, *nodes[i]);
|
loadNode(this, Transform(), os, *nodes[i]);
|
||||||
|
|
||||||
os << "</scene>" << endl;
|
os << "</scene>" << endl;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ class ColladaConverter {
|
||||||
public:
|
public:
|
||||||
inline ColladaConverter() {
|
inline ColladaConverter() {
|
||||||
m_srgb = false;
|
m_srgb = false;
|
||||||
|
m_mapSmallerSide = true;
|
||||||
|
m_xres = m_yres = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void convert(const std::string &inputFile,
|
void convert(const std::string &inputFile,
|
||||||
|
@ -16,6 +18,9 @@ public:
|
||||||
virtual std::string locateResource(const std::string &resource) = 0;
|
virtual std::string locateResource(const std::string &resource) = 0;
|
||||||
|
|
||||||
void setSRGB(bool srgb) { m_srgb = srgb; }
|
void setSRGB(bool srgb) { m_srgb = srgb; }
|
||||||
|
void setMapSmallerSide(bool mapSmallerSide) { m_mapSmallerSide = mapSmallerSide; }
|
||||||
|
void setResolution(int xres, int yres) { m_xres = xres; m_yres = yres; }
|
||||||
public:
|
public:
|
||||||
bool m_srgb;
|
bool m_srgb, m_mapSmallerSide;
|
||||||
|
int m_xres, m_yres;
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,20 +53,38 @@ void help() {
|
||||||
<< "Options/Arguments:" << endl
|
<< "Options/Arguments:" << endl
|
||||||
<< " -h Display this help text" << endl << endl
|
<< " -h Display this help text" << endl << endl
|
||||||
<< " -s Assume that colors are in sRGB space." << endl << endl
|
<< " -s Assume that colors are in sRGB space." << endl << endl
|
||||||
|
<< " -m Map the larger image side to the full field of view" << endl << endl
|
||||||
|
<< " -r <w>x<h> Override the image resolution to e.g. 1920×1080" << endl << endl
|
||||||
<< "Please see the documentation for more information." << endl;
|
<< "Please see the documentation for more information." << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
int colladaMain(int argc, char **argv) {
|
int colladaMain(int argc, char **argv) {
|
||||||
bool srgb = false;
|
bool srgb = false, mapSmallerSide = true;
|
||||||
char optchar;
|
char optchar, *end_ptr = NULL;
|
||||||
|
int xres = -1, yres = -1;
|
||||||
|
|
||||||
optind = 1;
|
optind = 1;
|
||||||
|
|
||||||
while ((optchar = getopt(argc, argv, "sh")) != -1) {
|
while ((optchar = getopt(argc, argv, "shmr:")) != -1) {
|
||||||
switch (optchar) {
|
switch (optchar) {
|
||||||
case 's':
|
case 's':
|
||||||
srgb = true;
|
srgb = true;
|
||||||
break;
|
break;
|
||||||
|
case 'm':
|
||||||
|
mapSmallerSide = false;
|
||||||
|
break;
|
||||||
|
case 'r': {
|
||||||
|
std::vector<std::string> tokens = tokenize(optarg, "x");
|
||||||
|
if (tokens.size() != 2)
|
||||||
|
SLog(EError, "Invalid resolution argument supplied!");
|
||||||
|
xres = strtol(tokens[0].c_str(), &end_ptr, 10);
|
||||||
|
if (*end_ptr != '\0')
|
||||||
|
SLog(EError, "Invalid resolution argument supplied!");
|
||||||
|
yres = strtol(tokens[1].c_str(), &end_ptr, 10);
|
||||||
|
if (*end_ptr != '\0')
|
||||||
|
SLog(EError, "Invalid resolution argument supplied!");
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
default:
|
default:
|
||||||
help();
|
help();
|
||||||
|
@ -81,6 +99,8 @@ int colladaMain(int argc, char **argv) {
|
||||||
|
|
||||||
ConsoleColladaConverter converter;
|
ConsoleColladaConverter converter;
|
||||||
converter.setSRGB(srgb);
|
converter.setSRGB(srgb);
|
||||||
|
converter.setResolution(xres, yres);
|
||||||
|
converter.setMapSmallerSide(mapSmallerSide);
|
||||||
converter.convert(argv[optind], "", argv[optind+1], argc > optind+2 ? argv[optind+2] : "");
|
converter.convert(argv[optind], "", argv[optind+1], argc > optind+2 ? argv[optind+2] : "");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -129,6 +149,10 @@ int ubi_main(int argc, char **argv) {
|
||||||
MTS_AUTORELEASE_END()
|
MTS_AUTORELEASE_END()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(WIN32)
|
||||||
|
setlocale(LC_ALL, "C");
|
||||||
|
#endif
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* An OpenGL context may be required for the GLU tesselator */
|
/* An OpenGL context may be required for the GLU tesselator */
|
||||||
ref<Session> session = Session::create();
|
ref<Session> session = Session::create();
|
||||||
|
@ -149,9 +173,6 @@ int ubi_main(int argc, char **argv) {
|
||||||
renderer->shutdown();
|
renderer->shutdown();
|
||||||
device->shutdown();
|
device->shutdown();
|
||||||
session->shutdown();
|
session->shutdown();
|
||||||
} catch (const std::exception &e) {
|
|
||||||
std::cerr << "Caught a critical exeption: " << e.what() << std::endl;
|
|
||||||
retval = -1;
|
|
||||||
} catch(const XMLException &toCatch) {
|
} catch(const XMLException &toCatch) {
|
||||||
SLog(EError, "Caught a Xerces exception: %s",
|
SLog(EError, "Caught a Xerces exception: %s",
|
||||||
XMLString::transcode(toCatch.getMessage()));
|
XMLString::transcode(toCatch.getMessage()));
|
||||||
|
@ -160,9 +181,6 @@ int ubi_main(int argc, char **argv) {
|
||||||
SLog(EError, "Caught a Xerces exception: %s",
|
SLog(EError, "Caught a Xerces exception: %s",
|
||||||
XMLString::transcode(toCatch.getMessage()));
|
XMLString::transcode(toCatch.getMessage()));
|
||||||
retval = -1;
|
retval = -1;
|
||||||
} catch (...) {
|
|
||||||
std::cerr << "Caught a critical exeption of unknown type!" << endl;
|
|
||||||
retval = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
XMLPlatformUtils::Terminate();
|
XMLPlatformUtils::Terminate();
|
||||||
|
|
|
@ -364,6 +364,10 @@ int main(int argc, char **argv) {
|
||||||
resolver->addPath(__ubi_bundlepath());
|
resolver->addPath(__ubi_bundlepath());
|
||||||
MTS_AUTORELEASE_END()
|
MTS_AUTORELEASE_END()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(WIN32)
|
||||||
|
setlocale(LC_NUMERIC, "C");
|
||||||
|
#endif
|
||||||
|
|
||||||
/* Initialize Xerces-C */
|
/* Initialize Xerces-C */
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -362,6 +362,10 @@ int main(int argc, char **argv) {
|
||||||
SLog(EError, "Could not find the required version of winsock.dll!");
|
SLog(EError, "Could not find the required version of winsock.dll!");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(WIN32)
|
||||||
|
setlocale(LC_NUMERIC, "C");
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __LINUX__
|
#ifdef __LINUX__
|
||||||
FileResolver *resolver = FileResolver::getInstance();
|
FileResolver *resolver = FileResolver::getInstance();
|
||||||
resolver->addPath("/usr/share/mitsuba");
|
resolver->addPath("/usr/share/mitsuba");
|
||||||
|
|
|
@ -128,6 +128,10 @@ int main(int argc, char *argv[]) {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(WIN32)
|
||||||
|
setlocale(LC_NUMERIC, "C");
|
||||||
|
#endif
|
||||||
|
|
||||||
#if !defined(WIN32)
|
#if !defined(WIN32)
|
||||||
/* Avoid zombies processes when running the server */
|
/* Avoid zombies processes when running the server */
|
||||||
struct sigaction sa;
|
struct sigaction sa;
|
||||||
|
|
Loading…
Reference in New Issue