OpenGL 4.2+ 和 shader_image_load_store 用于 3D 纹理不起作用



我试图弄清楚为什么我无法使用(现在内置的)shader_image_load_store扩展写入 3D 纹理。

我创建了两个简单的示例(在python中使其更容易):一个用于写入有效的2D纹理,另一个用于写入不起作用的3D纹理

(工作)2D版本如下:

#! /usr/bin/env python
from PyQt4 import QtGui, QtCore
from PyQt4.QtOpenGL import *
from OpenGL.GL import *
from OpenGL.GLU import *
import sys
iTexSize = 256

_vsClearSource =  """
    #version 440 compatibility
    void main() {
        gl_Position = ftransform();
        gl_FrontColor = gl_Color;
    }
    """     
_fsClearSource =   """
#version 440 compatibility
uniform int         iPrimitiveCount;
uniform int         iSliceIndex;
layout(size4x32, binding=0) coherent uniform   image2D volColorVolume;
const int iMaxTexSize = 255;  
void main() {
    ivec2 ivecVolumeCoordinate = ivec2(gl_FragCoord.x, gl_FragCoord.y ); //, iSliceIndex);
    vec4 vecVolumeValue =  vec4(0,1,0,1); // vec4(         float(iSlabIndex)/float(iPrimitiveCount)); //,0.0,0.0,0.0);
   imageStore(volColorVolume, ivecVolumeCoordinate, vecVolumeValue);                  
   gl_FragData[0] = vec4(1,0,1,1);
}    
    """    
_fsFillSource =   """
#version 440 compatibility
uniform int         iPrimitiveCount;
uniform int         iSliceIndex;
layout(size4x32, binding=0) coherent uniform   image2D volColorVolume;
const int iMaxTexSize = 255;  
void main() {
    ivec2 ivecVolumeCoordinate = ivec2(gl_FragCoord.x, gl_FragCoord.y );
    vec4 vecVolumeValue =  vec4( float(gl_FragCoord.x)  / float(iMaxTexSize)  , float(gl_FragCoord.y)  / float(iMaxTexSize)  , 0 ,  1  );
   imageStore(volColorVolume, ivecVolumeCoordinate, vecVolumeValue);                  
   gl_FragData[0] = vec4(1,0,1,1);
}    
"""       

class Viewer3DWidget(QGLWidget):
    def __init__(self, parent):
        QGLWidget.__init__(self, parent)
        self.uWidth = 0
        self.uHeight = 0
        self.texColorTexture = None
        self.fboRendering    = None
        self.texColorVolume = None
        self.vecBackgroundColor = (1.,1.,1.)
        self.vecDrawBuffers = [ GL_COLOR_ATTACHMENT0 , GL_COLOR_ATTACHMENT1 , GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, GL_COLOR_ATTACHMENT4, GL_COLOR_ATTACHMENT5 ]

        self.fSurfacesSpacing = 1.0
        self.fSurfacesTransparency = 1.0



    def initializeGL(self):
        self.shaShaderFill = QGLShaderProgram(self.context())
        self.shaShaderFill.addShaderFromSourceCode(QGLShader.Vertex, _vsClearSource)
        self.shaShaderFill.addShaderFromSourceCode(QGLShader.Fragment, _fsFillSource)   
        self.shaShaderFill.link()      
        self.shaShaderClear = QGLShaderProgram(self.context())
        self.shaShaderClear.addShaderFromSourceCode(QGLShader.Vertex, _vsClearSource)
        self.shaShaderClear.addShaderFromSourceCode(QGLShader.Fragment, _fsClearSource)   
        self.shaShaderClear.link()       

        glClearColor(1.0, 1.0, 1.0, 1.0)
        glClearDepth(1.0)

    def initRenderTargets(self):
        global iTexSize
        if (self.texColorTexture is None):
            self.texColorTexture = glGenTextures( 1 )
        if (self.fboRendering is  None):
            self.fboRendering = glGenFramebuffers(1)               
        glBindTexture( GL_TEXTURE_RECTANGLE, self.texColorTexture )
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA32F, iTexSize, iTexSize, 0, GL_RGBA, GL_FLOAT, None)

        glBindTexture(GL_TEXTURE_RECTANGLE, 0);
        glBindFramebuffer(GL_FRAMEBUFFER, self.fboRendering)
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE,  self.texColorTexture, 0)
        glBindFramebuffer(GL_FRAMEBUFFER, 0)    

    def deleteRenderTargets(self):
        if (self.fboAccumulation is  not  None):
            glDeleteFramebuffers(1,self.fboRendering)          
            self.fboAccumulation = None
        if (self.texColorTexture is not None):
            glDeleteTextures( self.texColorTexture )
            self.texColorTexture = None

    def initColorVolume(self):
        if (self.texColorVolume is None):
            self.texColorVolume = glGenTextures( 1 )

        glBindTexture( GL_TEXTURE_2D, self.texColorVolume )
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
        #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, iTexSize, iTexSize, 0, GL_RGBA, GL_FLOAT, None);

        glBindTexture(GL_TEXTURE_2D, 0);    

    def fillVolume(self, bClear):
        global iTexSize
        shaShader = self.shaShaderClear
        if(not bClear):
            shaShader = self.shaShaderFill
        if (not self.fboRendering):
            self.initRenderTargets() 
        if (not self.texColorVolume):
            self.initColorVolume()
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();            

        glBindImageTexture(0,self.texColorVolume,0,GL_FALSE,0,GL_READ_WRITE,GL_RGBA32F);
        glBindFramebuffer(GL_FRAMEBUFFER, self.fboRendering);
        glDrawBuffers(1, self.vecDrawBuffers);
        glClearColor(0, 0, 0, 0);
        glClear(GL_COLOR_BUFFER_BIT);            
        shaShader.bind()
        shaShader.setUniformValue("iPrimitiveCount", iTexSize)  
        shaShader.setUniformValue("volColorVolume", 0) 
        for i in range(iTexSize):

            shaShader.setUniformValue("iSliceIndex", i) 


            glBegin(GL_QUADS);
            glVertex2f(-1.0, -1.0); 
            glVertex2f(1.0, -1.0);
            glVertex2f(1.0, 1.0);
            glVertex2f(-1.0, 1.0);
            glEnd();

            #sync
            glMemoryBarrier(GL_ALL_BARRIER_BITS);

        glBindImageTexture(0,0,0,GL_FALSE,0,GL_READ_WRITE,GL_RGBA32F);
        shaShader.release()
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

    def paintGL(self):
        if  (self.uWidth is 0):
            return           
        if (not self.fboRendering):
            self.initRenderTargets() 
            self.initColorVolume()
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        self.fillVolume(True)
        #draw into the volume
        self.fillVolume(False)
        #slice the volume
        self.displayTexture()

        glFlush()


    def displayTexture(self):   #essentially not useable here
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
        glDisable(GL_BLEND)
        glDisable(GL_DEPTH_TEST);  
        glDisable(GL_LIGHTING)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glColor(1.0, 1.0,1.0)  
        glEnable(GL_TEXTURE_2D); 
        glBindTexture( GL_TEXTURE_2D, self.texColorVolume )
        glBegin(GL_QUADS);
        glTexCoord2f(0,0) #,0.5)
        glVertex2f(-1.0, -1.0); 
        glTexCoord2f(1,0) #,0.5)
        glVertex2f(1.0, -1.0);
        glTexCoord2f(1,1) #,0.5)
        glVertex2f(1.0, 1.0);
        glTexCoord2f(0,1) #,0.5)
        glVertex2f(-1.0, 1.0);
        glEnd();
        glBindTexture( GL_TEXTURE_2D, 0 )

    def resizeGL(self, widthInPixels, heightInPixels):
        if ((widthInPixels is not self.uWidth) or (heightInPixels is not self.uHeight)):        
            self.uWidth = widthInPixels
            self.uHeight = heightInPixels
            glViewport(0, 0, widthInPixels, heightInPixels)
            self.update()

class TestImageLoadStore2D(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle('TestImageLoadStore2D')
        self.statusBar().showMessage("Hello there")
        exit = QtGui.QAction("Exit", self)
        exit.setShortcut("Ctrl+Q")
        exit.setStatusTip('Exit application')
        self.connect(exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))

        self.viewer3D = Viewer3DWidget(self)
        self.setCentralWidget(self.viewer3D)
        self.resize(500,500)
    def closeEvent(self, event):
        event.accept()

if __name__ == '__main__':
    # app = QtGui.QApplication(['Python Qt OpenGL Demo'])
    app = QtGui.QApplication(sys.argv)
    window = TestImageLoadStore2D()
    window.show()
    sys.exit(app.exec_())

而非工作3D版本如下:

#! /usr/bin/env python
from PyQt4 import QtGui, QtCore
from PyQt4.QtOpenGL import *
from OpenGL.GL import *
from OpenGL.GLU import *
import sys
iTexSize = 256

_vsClearSource =  """
    #version 440 compatibility
    void main() {
        gl_Position = ftransform();
        gl_FrontColor = gl_Color;
    }
    """     
_fsClearSource =   """
#version 440 compatibility
uniform int         iPrimitiveCount;
uniform int         iSliceIndex;
layout(size4x32, binding=0) writeonly uniform   image3D volColorVolume;
//layout(rgba32f, binding=0) writeonly uniform   image3D volColorVolume;
const int iMaxTexSize = 255;  
void main() {
    ivec3 ivecVolumeCoordinate = ivec3(gl_FragCoord.x, gl_FragCoord.y, 0); //iSliceIndex);
    vec4 vecVolumeValue =  vec4(0,1,0,1); // vec4(         float(iSlabIndex)/float(iPrimitiveCount)); //,0.0,0.0,0.0);
   imageStore(volColorVolume, ivecVolumeCoordinate, vecVolumeValue); 
   memoryBarrier();                 
   gl_FragData[0] = vec4(1,0,1,1);
}    
    """    
_fsFillSource =   """
#version 440 compatibility
uniform int         iPrimitiveCount;
uniform int         iSliceIndex;
layout(size4x32, binding=0) writeonly uniform   image3D volColorVolume;
const int iMaxTexSize = 255;  
void main() {
    ivec3 ivecVolumeCoordinate = ivec3(gl_FragCoord.x, gl_FragCoord.y, 0); //iSliceIndex);
    vec4 vecVolumeValue =  vec4( float(gl_FragCoord.x)  / float(iMaxTexSize)  , float(gl_FragCoord.y)  / float(iMaxTexSize)  , float(iSliceIndex)/float(iPrimitiveCount) ,  1  );
   imageStore(volColorVolume, ivecVolumeCoordinate, vecVolumeValue);   
   memoryBarrier();               
   gl_FragData[0] = vec4(1,0,1,1);
}    
"""       

class Viewer3DWidget(QGLWidget):
    def __init__(self, parent):
        QGLWidget.__init__(self, parent)
        self.uWidth = 0
        self.uHeight = 0
        self.texColorTexture = None
        self.fboRendering    = None
        self.texColorVolume = None
        self.vecBackgroundColor = (1.,1.,1.)
        self.vecDrawBuffers = [ GL_COLOR_ATTACHMENT0 , GL_COLOR_ATTACHMENT1 , GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, GL_COLOR_ATTACHMENT4, GL_COLOR_ATTACHMENT5 ]

        self.fSurfacesSpacing = 1.0
        self.fSurfacesTransparency = 1.0
        self.fZCoord = 0.0

    def setZCoordinate(self, fZCoordinate):
        self.fZCoord = fZCoordinate
        self.update()

    def initializeGL(self):
        self.shaShaderFill = QGLShaderProgram(self.context())
        self.shaShaderFill.addShaderFromSourceCode(QGLShader.Vertex, _vsClearSource)
        self.shaShaderFill.addShaderFromSourceCode(QGLShader.Fragment, _fsFillSource)   
        self.shaShaderFill.link()      
        self.shaShaderClear = QGLShaderProgram(self.context())
        self.shaShaderClear.addShaderFromSourceCode(QGLShader.Vertex, _vsClearSource)
        self.shaShaderClear.addShaderFromSourceCode(QGLShader.Fragment, _fsClearSource)   
        self.shaShaderClear.link()       

        glClearColor(1.0, 1.0, 1.0, 1.0)
        glClearDepth(1.0)

    def initRenderTargets(self):
        global iTexSize
        if (self.texColorTexture is None):
            self.texColorTexture = glGenTextures( 1 )
        if (self.fboRendering is  None):
            self.fboRendering = glGenFramebuffers(1)               
        glBindTexture( GL_TEXTURE_RECTANGLE, self.texColorTexture )
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA32F, iTexSize, iTexSize, 0, GL_RGBA, GL_FLOAT, None)

        glBindTexture(GL_TEXTURE_RECTANGLE, 0);
        glBindFramebuffer(GL_FRAMEBUFFER, self.fboRendering)
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE,  self.texColorTexture, 0)
        glBindFramebuffer(GL_FRAMEBUFFER, 0)    

    def deleteRenderTargets(self):
        if (self.fboAccumulation is  not  None):
            glDeleteFramebuffers(1,self.fboRendering)          
            self.fboAccumulation = None
        if (self.texColorTexture is not None):
            glDeleteTextures( self.texColorTexture )
            self.texColorTexture = None

    def initColorVolume(self):
        if (self.texColorVolume is None):
            self.texColorVolume = glGenTextures( 1 )

        glBindTexture( GL_TEXTURE_3D, self.texColorVolume )
        glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP)
        glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA32F, iTexSize, iTexSize, iTexSize, 0, GL_RGBA, GL_FLOAT, None);

        glBindTexture(GL_TEXTURE_3D, 0);    

    def fillVolume(self, bClear):
        global iTexSize
        shaShader = self.shaShaderClear
        if(not bClear):
            shaShader = self.shaShaderFill
        if (not self.fboRendering):
            self.initRenderTargets() 
        if (not self.texColorVolume):
            self.initColorVolume()
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();            

        glBindImageTexture(0,self.texColorVolume,0,GL_FALSE,0,GL_WRITE_ONLY,GL_RGBA32F);
        glBindFramebuffer(GL_FRAMEBUFFER, self.fboRendering);
        glDrawBuffers(1, self.vecDrawBuffers);
        glClearColor(0, 0, 0, 0);
        glClear(GL_COLOR_BUFFER_BIT);            
        shaShader.bind()
        shaShader.setUniformValue("iPrimitiveCount", iTexSize)  
        shaShader.setUniformValue("volColorVolume", 0) 
        for i in range(iTexSize):

            shaShader.setUniformValue("iSliceIndex", i) 


            glBegin(GL_QUADS);
            glVertex2f(-1.0, -1.0); 
            glVertex2f(1.0, -1.0);
            glVertex2f(1.0, 1.0);
            glVertex2f(-1.0, 1.0);
            glEnd();

            #sync
            glMemoryBarrier(GL_ALL_BARRIER_BITS);
        glMemoryBarrier(GL_ALL_BARRIER_BITS);
        glBindImageTexture(0,0,0,GL_FALSE,0,GL_WRITE_ONLY,GL_RGBA32F);
        shaShader.release()
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

    def paintGL(self):
        if  (self.uWidth is 0):
            return           
        if (not self.fboRendering):
            self.initRenderTargets() 
            self.initColorVolume()
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        self.fillVolume(True)
        #draw into the volume
        #self.fillVolume(False)
        #slice the volume
        self.displayTexture()

        glFlush()


    def displayTexture(self):   #essentially not useable here
        glMatrixMode( GL_PROJECTION )
        glLoadIdentity()
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
        glDisable(GL_BLEND)
        glDisable(GL_DEPTH_TEST);  
        glDisable(GL_LIGHTING)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glColor(1.0, 1.0,1.0)  
        glEnable(GL_TEXTURE_3D); 
        glBindTexture( GL_TEXTURE_3D, self.texColorVolume )
        fZCoord = self.fZCoord        
        glBegin(GL_QUADS);
        glTexCoord3f(0,0,fZCoord)
        glVertex2f(-1.0, -1.0); 
        glTexCoord3f(1,0,fZCoord)
        glVertex2f(1.0, -1.0);
        glTexCoord3f(1,1,fZCoord)
        glVertex2f(1.0, 1.0);
        glTexCoord3f(0,1,fZCoord)
        glVertex2f(-1.0, 1.0);
        glEnd();
        glBindTexture( GL_TEXTURE_3D, 0 )

    def resizeGL(self, widthInPixels, heightInPixels):
        if ((widthInPixels is not self.uWidth) or (heightInPixels is not self.uHeight)):        
            self.uWidth = widthInPixels
            self.uHeight = heightInPixels
            glViewport(0, 0, widthInPixels, heightInPixels)
            self.update()


class TestImageLoadStore3D(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle('TestImageLoadStore3D')
        self.statusBar().showMessage("Hello there")
        exit = QtGui.QAction("Exit", self)
        exit.setShortcut("Ctrl+Q")
        exit.setStatusTip('Exit application')
        self.connect(exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))

        self.setToolTip('This is a window, or <b>something</b>')
        self.viewer3D = Viewer3DWidget(self)
        parentWidget = QtGui.QWidget()
        slider1 = QtGui.QSlider(QtCore.Qt.Horizontal, None)
        slider1.setRange(0,10000)
        slider1.setValue(5000)
        slider1.setMaximumWidth(120)
        slider1.valueChanged.connect(self.slider1Handler)
        vbox = QtGui.QVBoxLayout()
        vbox.addWidget(slider1)
        vbox.addStretch(1)
        self.viewer3D.setSizePolicy( QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding )
        hbox = QtGui.QHBoxLayout()
        hbox.addLayout(vbox)
        hbox.addWidget(self.viewer3D)
        parentWidget.setLayout(hbox)        
        self.setCentralWidget(parentWidget)

        self.resize(500,500)
    def closeEvent(self, event):
        event.accept()
    def slider1Handler(self, iVal):    
        fVal = iVal / 10000.0
        #print "zcoord: ",fVal
        self.viewer3D.setZCoordinate(fVal)           
if __name__ == '__main__':
    # app = QtGui.QApplication(['Python Qt OpenGL Demo'])
    app = QtGui.QApplication(sys.argv)
    window = TestImageLoadStore3D()
    window.show()
    sys.exit(app.exec_())

代码不是特别整洁,也是因为我复制/粘贴/修改了一些较旧的 pyopengl 代码。问题是写在3D纹理中的值绝对没有意义。我使用最新版本的PyOpenGL(实验版本),Quadro K5000和最新的驱动程序(332.76)运行它,这些驱动程序也为OpenGL 4.4提供支持。

不确定我可能做错了什么,也是因为我没有找到很多写入 3D 纹理的例子(实际上没有,我也查看了最新版本的红皮书)

有人可以开导我吗?

您的问题出在碎片着色器中:

layout(size4x32, binding=0) writeonly uniform   image3D volColorVolume;

您正在通过以下方式绑定 3D 纹理

glBindImageTexture(0,self.texColorVolume,0,GL_FALSE,0,GL_WRITE_ONLY,GL_RGBA32F);

让我引用OpenGL 4.4规范第8.26节"纹理图像加载和存储"(强调我的):

如果由纹理标识的纹理是一维数组, 二维数组、三维、立方体贴图、立方体贴图数组或 二维多重采样数组纹理,可以绑定 整个纹理级别或单个图层或面 纹理级别。如果分层为 TRUE,则绑定整个关卡。如果 layered 为 FALSE,只有由 layer 标识的单个图层才会 绑定。当分层为 FALSE 时,单个绑定图层被视为 图像访问的不同纹理目标:

    一维
  • 数组纹理层被视为一维纹理;
  • 二维阵列、三维、立方体
  • 贴图、立方体贴图阵列纹理层被视为二维纹理;和
  • 二维多重采样
  • 数组纹理被视为二维多重采样纹理。

因此,如果您只是将 layered 参数设置为 GL_FALSE 的 3D 纹理的一层(就像您当前所做的那样),它将表现得好像它只是一个 2D 纹理,因此只需使用image2D并使用 2D 坐标访问它,或者将其绑定为设置为 GL_TRUElayered和您可以写入特定图层的有效图层范围/使用image3D和三维图像坐标的纹理切片。

你在

python中有60个分号。Python 不使用分号。事实上,我很确定python是唯一真正不使用分号的编程语言。

相关内容

  • 没有找到相关文章